2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
10 # | Copyright Mathias Kettner 2014 mk@mathias-kettner.de |
11 # +------------------------------------------------------------------+
13 # This file is part of Check_MK.
14 # The official homepage is at http://mathias-kettner.de/check_mk.
16 # check_mk is free software; you can redistribute it and/or modify it
17 # under the terms of the GNU General Public License as published by
18 # the Free Software Foundation in version 2. check_mk is distributed
19 # in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
20 # out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
21 # PARTICULAR PURPOSE. See the GNU General Public License for more de-
22 # tails. You should have received a copy of the GNU General Public
23 # License along with GNU Make; see the file COPYING. If not, write
24 # to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
25 # Boston, MA 02110-1301 USA.
35 from cStringIO
import StringIO
36 from typing
import NamedTuple
40 import cmk
.utils
.paths
41 import cmk
.utils
.tty
as tty
42 import cmk
.utils
.werks
43 import cmk
.utils
.debug
46 logger
= cmk
.utils
.log
.get_logger(__name__
)
50 # TODO: Subclass MKGeneralException()?
51 class PackageException(Exception):
52 def __init__(self
, reason
):
54 super(PackageException
, self
).__init
__(reason
)
60 pac_dir
= cmk
.utils
.paths
.omd_root
+ "/var/check_mk/packages/"
62 # TODO: OMD: Pack this path and remote this makedirs call
68 # order matters! See function _get_permissions
70 (cmk
.utils
.paths
.checks_dir
, 0644),
71 (cmk
.utils
.paths
.local_checks_dir
, 0644),
72 (cmk
.utils
.paths
.notifications_dir
, 0755),
73 (cmk
.utils
.paths
.local_notifications_dir
, 0755),
74 (cmk
.utils
.paths
.inventory_dir
, 0644),
75 (cmk
.utils
.paths
.local_inventory_dir
, 0644),
76 (cmk
.utils
.paths
.check_manpages_dir
, 0644),
77 (cmk
.utils
.paths
.local_check_manpages_dir
, 0644),
78 (cmk
.utils
.paths
.agents_dir
, 0755),
79 (cmk
.utils
.paths
.local_agents_dir
, 0755),
80 (cmk
.utils
.paths
.web_dir
, 0644),
81 (cmk
.utils
.paths
.local_web_dir
, 0644),
82 (cmk
.utils
.paths
.pnp_templates_dir
, 0644),
83 (cmk
.utils
.paths
.local_pnp_templates_dir
, 0644),
84 (cmk
.utils
.paths
.doc_dir
, 0644),
85 (cmk
.utils
.paths
.local_doc_dir
, 0644),
86 (cmk
.utils
.paths
.locale_dir
, 0644),
87 (cmk
.utils
.paths
.local_locale_dir
, 0644),
88 (cmk
.utils
.paths
.local_bin_dir
, 0755),
89 (os
.path
.join(cmk
.utils
.paths
.local_lib_dir
, "nagios", "plugins"), 0755),
90 (cmk
.utils
.paths
.local_lib_dir
, 0644),
91 (cmk
.utils
.paths
.local_mib_dir
, 0644),
92 (os
.path
.join(cmk
.utils
.paths
.share_dir
, "alert_handlers"), 0755),
93 (os
.path
.join(cmk
.utils
.paths
.local_share_dir
, "alert_handlers"), 0755),
94 (str(cmk
.ec
.export
.mkp_rule_pack_dir()), 0644),
98 def _get_permissions(path
):
99 """Determine permissions by the first matching beginning of 'path'"""
100 for path_begin
, perm
in PERM_MAP
:
101 if path
.startswith(path_begin
):
103 raise PackageException("could not determine permissions for %r" % path
)
106 PackagePart
= NamedTuple("PackagePart", [
113 PackagePart("checks", "Checks", cmk
.utils
.paths
.local_checks_dir
),
114 PackagePart("notifications", "Notification scripts", cmk
.utils
.paths
.local_notifications_dir
),
115 PackagePart("inventory", "Inventory plugins", cmk
.utils
.paths
.local_inventory_dir
),
116 PackagePart("checkman", "Checks' man pages", cmk
.utils
.paths
.local_check_manpages_dir
),
117 PackagePart("agents", "Agents", cmk
.utils
.paths
.local_agents_dir
),
118 PackagePart("web", "Multisite extensions", cmk
.utils
.paths
.local_web_dir
),
119 PackagePart("pnp-templates", "PNP4Nagios templates", cmk
.utils
.paths
.local_pnp_templates_dir
),
120 PackagePart("doc", "Documentation files", cmk
.utils
.paths
.local_doc_dir
),
121 PackagePart("locales", "Localizations", cmk
.utils
.paths
.local_locale_dir
),
122 PackagePart("bin", "Binaries", cmk
.utils
.paths
.local_bin_dir
),
123 PackagePart("lib", "Libraries", cmk
.utils
.paths
.local_lib_dir
),
124 PackagePart("mibs", "SNMP MIBs", cmk
.utils
.paths
.local_mib_dir
),
125 PackagePart("alert_handlers", "Alert handlers",
126 cmk
.utils
.paths
.local_share_dir
+ "/alert_handlers"),
130 PackagePart("ec_rule_packs", "Event Console rule packs",
131 str(cmk
.ec
.export
.mkp_rule_pack_dir())),
134 package_ignored_files
= {
136 "nagios/plugins/README.txt",
137 # it's a symlink to the nagios directory. All files would be doubled.
138 # So better ignore this directory to prevent confusions.
144 def get_package_parts():
145 return _package_parts
148 def packaging_usage():
149 sys
.stdout
.write("""Usage: check_mk [-v] -P|--package COMMAND [ARGS]
151 Available commands are:
152 create NAME ... Collect unpackaged files into new package NAME
153 pack NAME ... Create package file from installed package
154 release NAME ... Drop installed package NAME, release packaged files
155 find ... Find and display unpackaged files
156 list ... List all installed packages
157 list NAME ... List files of installed package
158 list PACK.mkp ... List files of uninstalled package file
159 show NAME ... Show information about installed package
160 show PACK.mkp ... Show information about uninstalled package file
161 install PACK.mkp ... Install or update package from file PACK.mkp
162 remove NAME ... Uninstall package NAME
164 -v enables verbose output
166 Package files are located in %s.
170 def do_packaging(args
):
178 "create": package_create
,
179 "release": package_release
,
180 "list": package_list
,
181 "find": package_find
,
182 "show": package_info
,
183 "pack": package_pack
,
184 "remove": package_remove
,
185 "install": package_install
,
187 f
= commands
.get(command
)
191 except PackageException
as e
:
192 logger
.error("%s" % e
)
195 allc
= commands
.keys()
197 allc
= [tty
.bold
+ c
+ tty
.normal
for c
in allc
]
198 logger
.error("Invalid packaging command. Allowed are: %s and %s.", ", ".join(allc
[:-1]),
203 def package_list(args
):
206 show_package_contents(name
)
208 if logger
.is_verbose():
210 for pacname
in all_package_names():
211 package
= read_package_info(pacname
)
212 table
.append((pacname
, package
["title"], package
["num_files"]))
213 tty
.print_table(["Name", "Title", "Files"], [tty
.bold
, "", ""], table
)
215 for pacname
in all_package_names():
216 sys
.stdout
.write("%s\n" % pacname
)
219 def package_info(args
):
221 raise PackageException("Usage: check_mk -P show NAME|PACKAGE.mkp")
223 show_package_info(name
)
226 def show_package_contents(name
):
227 show_package(name
, False)
230 def show_package_info(name
):
231 show_package(name
, True)
234 def show_package(name
, show_info
=False):
236 if name
.endswith(pac_ext
):
237 tar
= tarfile
.open(name
, "r:gz")
238 info
= tar
.extractfile("info")
239 package
= parse_package_info(info
.read())
241 package
= read_package_info(name
)
243 raise PackageException("No such package %s." % name
)
245 sys
.stdout
.write("Package file: %s%s\n" % (pac_dir
, name
))
246 except PackageException
:
248 except Exception as e
:
249 raise PackageException("Cannot open package %s: %s" % (name
, e
))
252 sys
.stdout
.write("Name: %s\n" % package
["name"])
253 sys
.stdout
.write("Version: %s\n" % package
["version"])
254 sys
.stdout
.write("Packaged on Check_MK Version: %s\n" % package
["version.packaged"])
255 sys
.stdout
.write("Required Check_MK Version: %s\n" % package
["version.min_required"])
256 sys
.stdout
.write("Title: %s\n" % package
["title"])
257 sys
.stdout
.write("Author: %s\n" % package
["author"])
258 sys
.stdout
.write("Download-URL: %s\n" % package
["download_url"])
259 sys
.stdout
.write("Files: %s\n" % \
260 " ".join([ "%s(%d)" % (part
, len(fs
)) for part
, fs
in package
["files"].items() ]))
261 sys
.stdout
.write("Description:\n %s\n" % package
["description"])
263 if logger
.is_verbose():
264 sys
.stdout
.write("Files in package %s:\n" % name
)
265 for part
in get_package_parts():
266 files
= package
["files"].get(part
.ident
, [])
268 sys
.stdout
.write(" %s%s%s:\n" % (tty
.bold
, part
.title
, tty
.normal
))
270 sys
.stdout
.write(" %s\n" % f
)
272 for part
in get_package_parts():
273 for fn
in package
["files"].get(part
.ident
, []):
274 sys
.stdout
.write(part
.path
+ "/" + fn
+ "\n")
277 def package_create(args
):
279 raise PackageException("Usage: check_mk -P create NAME")
282 if read_package_info(pacname
):
283 raise PackageException("Package %s already existing." % pacname
)
285 logger
.verbose("Creating new package %s...", pacname
)
288 "title": "Title of %s" % pacname
,
290 "description": "Please add a description here",
292 "version.packaged": cmk
.__version
__,
293 "version.min_required": cmk
.__version
__,
294 "author": "Add your name here",
295 "download_url": "http://example.com/%s/" % pacname
,
299 for part
in get_package_parts():
300 files
= unpackaged_files_in_dir(part
.ident
, part
.path
)
301 filelists
[part
.ident
] = files
302 num_files
+= len(files
)
304 logger
.verbose(" %s%s%s:", tty
.bold
, part
.title
, tty
.normal
)
306 logger
.verbose(" %s", f
)
308 write_package_info(package
)
309 logger
.verbose("New package %s created with %d files.", pacname
, num_files
)
310 logger
.verbose("Please edit package details in %s%s%s", tty
.bold
, pac_dir
+ pacname
, tty
.normal
)
313 def package_find(_no_args
):
315 for part
in get_package_parts() + config_parts
:
316 files
= unpackaged_files_in_dir(part
.ident
, part
.path
)
319 logger
.verbose("Unpackaged files:")
322 logger
.verbose(" %s%s%s:", tty
.bold
, part
.title
, tty
.normal
)
324 if logger
.is_verbose():
325 logger
.verbose(" %s", f
)
327 logger
.info("%s/%s", part
.path
, f
)
330 logger
.verbose("No unpackaged files found.")
333 def release_package(pacname
):
334 if not pacname
or not package_exists(pacname
):
335 raise PackageException("Package %s not installed or corrupt." % pacname
)
337 package
= read_package_info(pacname
)
338 logger
.verbose("Releasing files of package %s into freedom...", pacname
)
339 for part
in get_package_parts() + config_parts
:
340 filenames
= package
["files"].get(part
.ident
, [])
341 if len(filenames
) > 0:
342 logger
.verbose(" %s%s%s:", tty
.bold
, part
.title
, tty
.normal
)
344 logger
.verbose(" %s", f
)
345 if part
.ident
== 'ec_rule_packs':
346 cmk
.ec
.export
.release_packaged_rule_packs(filenames
)
347 remove_package_info(pacname
)
350 def package_release(args
):
352 raise PackageException("Usage: check_mk -P release NAME")
354 release_package(pacname
)
357 def package_exists(pacname
):
358 pacpath
= pac_dir
+ pacname
359 return os
.path
.exists(pacpath
)
362 def package_pack(args
):
364 raise PackageException("Usage: check_mk -P pack NAME")
366 # Make sure, user is not in data directories of Check_MK
367 abs_curdir
= os
.path
.abspath(os
.curdir
)
368 for directory
in [cmk
.utils
.paths
.var_dir
369 ] + [p
.path
for p
in get_package_parts() + config_parts
]:
370 if abs_curdir
== directory
or abs_curdir
.startswith(directory
+ "/"):
371 raise PackageException(
373 "Please leave the directories of Check_MK before creating\n"
374 "a packet file. Foreign files lying around here will mix up things." % p
)
377 package
= read_package_info(pacname
)
379 raise PackageException("Package %s not existing or corrupt." % pacname
)
380 tarfilename
= "%s-%s%s" % (pacname
, package
["version"], pac_ext
)
381 logger
.verbose("Packing %s into %s...", pacname
, tarfilename
)
382 create_mkp_file(package
, file_name
=tarfilename
)
383 logger
.verbose("Successfully created %s", tarfilename
)
386 def create_mkp_file(package
, file_name
=None, file_object
=None):
387 package
["version.packaged"] = cmk
.__version
__
389 def create_tar_info(filename
, size
):
390 info
= tarfile
.TarInfo()
391 info
.mtime
= time
.time()
396 info
.type = tarfile
.REGTYPE
400 tar
= tarfile
.open(name
=file_name
, fileobj
=file_object
, mode
="w:gz")
402 # add the regular info file (Python format)
403 info_file
= StringIO(pprint
.pformat(package
))
404 info
= create_tar_info("info", len(info_file
.getvalue()))
405 tar
.addfile(info
, info_file
)
407 # add the info file a second time (JSON format) for external tools
408 info_file
= StringIO(json
.dumps(package
))
409 info
= create_tar_info("info.json", len(info_file
.getvalue()))
410 tar
.addfile(info
, info_file
)
412 # Now pack the actual files into sub tars
413 for part
in get_package_parts() + config_parts
:
414 filenames
= package
["files"].get(part
.ident
, [])
415 if len(filenames
) > 0:
416 logger
.verbose(" %s%s%s:", tty
.bold
, part
.title
, tty
.normal
)
418 logger
.verbose(" %s", f
)
419 subtarname
= part
.ident
+ ".tar"
420 subdata
= subprocess
.check_output(
421 ["tar", "cf", "-", "--dereference", "--force-local", "-C", part
.path
] + filenames
)
422 info
= create_tar_info(subtarname
, len(subdata
))
423 tar
.addfile(info
, StringIO(subdata
))
427 def package_remove(args
):
429 raise PackageException("Usage: check_mk -P remove NAME")
431 package
= read_package_info(pacname
)
433 raise PackageException("No such package %s." % pacname
)
435 logger
.verbose("Removing package %s...", pacname
)
436 remove_package(package
)
437 logger
.verbose("Successfully removed package %s.", pacname
)
440 def remove_package(package
):
441 for part
in get_package_parts() + config_parts
:
442 filenames
= package
["files"].get(part
.ident
, [])
443 if len(filenames
) > 0:
444 logger
.verbose(" %s%s%s", tty
.bold
, part
.title
, tty
.normal
)
446 logger
.verbose(" %s", fn
)
448 path
= part
.path
+ "/" + fn
449 if part
.ident
== 'ec_rule_packs':
450 cmk
.ec
.export
.remove_packaged_rule_packs(filenames
)
453 except Exception as e
:
454 if cmk
.utils
.debug
.enabled():
456 raise Exception("Cannot remove %s: %s\n" % (path
, e
))
458 os
.remove(pac_dir
+ package
["name"])
461 def create_package(pkg_info
):
462 pacname
= pkg_info
["name"]
463 if package_exists(pacname
):
464 raise PackageException("Packet already exists.")
466 validate_package_files(pacname
, pkg_info
["files"])
467 write_package_info(pkg_info
)
470 def edit_package(pacname
, new_package_info
):
471 if not package_exists(pacname
):
472 raise PackageException("No such package")
474 # Renaming: check for collision
475 if pacname
!= new_package_info
["name"]:
476 if package_exists(new_package_info
["name"]):
477 raise PackageException(
478 "Cannot rename package: a package with that name already exists.")
480 validate_package_files(pacname
, new_package_info
["files"])
482 remove_package_info(pacname
)
483 write_package_info(new_package_info
)
486 # Packaged files must either be unpackaged or already
487 # belong to that package
488 def validate_package_files(pacname
, files
):
490 for package_name
in all_package_names():
491 packages
[package_name
] = read_package_info(package_name
)
493 for part
in get_package_parts():
494 validate_package_files_part(packages
, pacname
, part
.ident
, part
.path
,
495 files
.get(part
.ident
, []))
498 def validate_package_files_part(packages
, pacname
, part
, directory
, rel_paths
):
499 for rel_path
in rel_paths
:
500 path
= os
.path
.join(directory
, rel_path
)
501 if not os
.path
.exists(path
):
502 raise PackageException("File %s does not exist." % path
)
504 for other_pacname
, other_package_info
in packages
.items():
505 for other_rel_path
in other_package_info
["files"].get(part
, []):
506 if other_rel_path
== rel_path
and other_pacname
!= pacname
:
507 raise PackageException(
508 "File %s does already belong to package %s" % (path
, other_pacname
))
511 def package_install(args
):
513 raise PackageException("Usage: check_mk -P install NAME")
515 if not os
.path
.exists(path
):
516 raise PackageException("No such file %s." % path
)
518 return install_package(file_name
=path
)
521 def install_package(file_name
=None, file_object
=None):
522 tar
= tarfile
.open(name
=file_name
, fileobj
=file_object
, mode
="r:gz")
523 package
= parse_package_info(tar
.extractfile("info").read())
525 verify_check_mk_version(package
)
527 pacname
= package
["name"]
528 old_package
= read_package_info(pacname
)
530 logger
.verbose("Updating %s from version %s to %s.", pacname
, old_package
["version"],
534 logger
.verbose("Installing %s version %s.", pacname
, package
["version"])
537 # Before installing check for conflicts
539 for part
in get_package_parts() + config_parts
:
540 packaged
= packaged_files_in_dir(part
.ident
)
542 keep_files
[part
.ident
] = keep
545 old_files
= old_package
["files"].get(part
.ident
, [])
547 for fn
in package
["files"].get(part
.ident
, []):
548 path
= os
.path
.join(part
.path
, fn
)
549 if update
and fn
in old_files
:
552 raise PackageException("File conflict: %s is part of another package." % path
)
553 elif os
.path
.exists(path
):
554 raise PackageException("File conflict: %s already existing." % path
)
556 # Now install files, but only unpack files explicitely listed
557 for part
in get_package_parts() + config_parts
:
558 filenames
= package
["files"].get(part
.ident
, [])
559 if len(filenames
) > 0:
560 logger
.verbose(" %s%s%s:", tty
.bold
, part
.title
, tty
.normal
)
562 logger
.verbose(" %s", fn
)
564 # make sure target directory exists
565 if not os
.path
.exists(part
.path
):
566 logger
.verbose(" Creating directory %s", part
.path
)
567 os
.makedirs(part
.path
)
569 tarsource
= tar
.extractfile(part
.ident
+ ".tar")
571 tardest
= subprocess
.Popen(
572 ["tar", "xf", "-", "-C", part
.path
] + filenames
, stdin
=subprocess
.PIPE
)
574 data
= tarsource
.read(4096)
577 tardest
.stdin
.write(data
)
579 tardest
.stdin
.close()
582 # Fix permissions of extracted files
583 for filename
in filenames
:
584 path
= os
.path
.join(part
.path
, filename
)
585 desired_perm
= _get_permissions(path
)
586 has_perm
= os
.stat(path
).st_mode
& 07777
587 if has_perm
!= desired_perm
:
588 logger
.verbose(" Fixing permissions of %s: %04o -> %04o", path
, has_perm
,
590 os
.chmod(path
, desired_perm
)
592 if part
.ident
== 'ec_rule_packs':
593 cmk
.ec
.export
.add_rule_pack_proxies(filenames
)
595 # In case of an update remove files from old_package not present in new one
597 for part
in get_package_parts() + config_parts
:
598 filenames
= old_package
["files"].get(part
.ident
, [])
599 keep
= keep_files
.get(part
.ident
, [])
602 path
= os
.path
.join(part
.path
, fn
)
603 logger
.verbose("Removing outdated file %s.", path
)
606 except Exception as e
:
607 logger
.error("Error removing %s: %s", path
, e
)
609 if part
.ident
== 'ec_rule_packs':
610 to_remove
= [fn
for fn
in filenames
if fn
not in keep
]
611 cmk
.ec
.export
.remove_packaged_rule_packs(to_remove
, delete_export
=False)
613 # Last but not least install package file
614 write_package_info(package
)
618 # Checks whether or not the minimum required Check_MK version is older than the
619 # current Check_MK version. Raises an exception if not. When the Check_MK version
620 # can not be parsed or is a daily build, the check is simply passing without error.
621 def verify_check_mk_version(package
):
622 min_version
= package
["version.min_required"]
623 cmk_version
= cmk
.__version
__
625 if cmk_base
.utils
.is_daily_build_version(min_version
):
626 min_branch
= cmk_base
.utils
.branch_of_daily_build(min_version
)
627 if min_branch
== "master":
628 return # can not check exact version
630 # use the branch name (e.g. 1.2.8 as min version)
631 min_version
= min_branch
633 if cmk_base
.utils
.is_daily_build_version(cmk_version
):
634 branch
= cmk_base
.utils
.branch_of_daily_build(cmk_version
)
635 if branch
== "master":
636 return # can not check exact version
638 # use the branch name (e.g. 1.2.8 as min version)
643 compatible
= cmk
.utils
.werks
.parse_check_mk_version(min_version
) \
644 <= cmk
.utils
.werks
.parse_check_mk_version(cmk_version
)
646 # Be compatible: When a version can not be parsed, then skip this check
647 if cmk
.utils
.debug
.enabled():
652 raise PackageException("The package requires Check_MK version %s, "
653 "but you have %s installed." % (min_version
, cmk_version
))
656 def files_in_dir(part
, directory
, prefix
=""):
657 if directory
is None or not os
.path
.exists(directory
):
660 # Handle case where one part-directory lies below another
661 taboo_dirs
= [p
.path
for p
in get_package_parts() + config_parts
if p
.ident
!= part
]
662 if directory
in taboo_dirs
:
666 files
= os
.listdir(directory
)
668 if f
in ['.', '..'] or f
.startswith('.') or f
.endswith('~') or f
.endswith(".pyc"):
671 ignored
= package_ignored_files
.get(part
, [])
672 if prefix
+ f
in ignored
:
675 path
= directory
+ "/" + f
676 if os
.path
.isdir(path
):
677 result
+= files_in_dir(part
, path
, prefix
+ f
+ "/")
679 result
.append(prefix
+ f
)
684 def unpackaged_files():
686 for part
in get_package_parts() + config_parts
:
687 unpackaged
[part
.ident
] = unpackaged_files_in_dir(part
.ident
, part
.path
)
691 def package_part_info():
693 for part
in get_package_parts() + config_parts
:
695 files
= os
.listdir(part
.path
)
699 part_info
[part
.ident
] = {
701 "permissions": map(_get_permissions
, [os
.path
.join(part
.path
, f
) for f
in files
]),
709 def unpackaged_files_in_dir(part
, directory
):
710 return [f
for f
in files_in_dir(part
, directory
) if f
not in packaged_files_in_dir(part
)]
713 def packaged_files_in_dir(part
):
715 for pacname
in all_package_names():
716 package
= read_package_info(pacname
)
718 result
+= package
["files"].get(part
, [])
722 def read_package_info(pacname
):
724 package
= parse_package_info(file(pac_dir
+ pacname
).read())
725 package
["name"] = pacname
# do not trust package content
726 num_files
= sum([len(fl
) for fl
in package
["files"].values()])
727 package
["num_files"] = num_files
732 logger
.verbose("Ignoring invalid package file '%s%s'. Please remove it from %s!", pac_dir
,
737 def write_package_info(package
):
738 file(pac_dir
+ package
["name"], "w").write(pprint
.pformat(package
) + "\n")
741 def remove_package_info(pacname
):
742 os
.remove(pac_dir
+ pacname
)
745 def all_package_names():
746 return sorted([p
for p
in os
.listdir(pac_dir
) if p
not in ['.', '..']])
749 def parse_package_info(python_string
):
750 return ast
.literal_eval(python_string
)