pax_global_header00006660000000000000000000000064132273730270014520gustar00rootroot0000000000000052 comment=aa829bd53bb25c965c71929611aca15ffeff502e python-aptly-0.12.10/000077500000000000000000000000001322737302700143315ustar00rootroot00000000000000python-aptly-0.12.10/.gitignore000066400000000000000000000000671322737302700163240ustar00rootroot00000000000000*.swp *.o *.pyc .DS_Store .ropeproject *.egg-info dist python-aptly-0.12.10/.pep8speaks.yml000066400000000000000000000000751322737302700172170ustar00rootroot00000000000000pycodestyle: ignore: - E501 scanner: diff_only: true python-aptly-0.12.10/LICENSE000066400000000000000000000432541322737302700153460ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. python-aptly-0.12.10/README.rst000066400000000000000000000143771322737302700160340ustar00rootroot00000000000000============ python-aptly ============ Aptly REST API client and useful tooling Publisher ========= Publisher is tooling for easier maintenance of complex repository management workflows. This is how workflow can look like and what publisher can do for you: .. image:: ./doc/aptly-publisher.png :align: center Features -------- - Create or update publish from latest snapshots - it takes configuration in yaml format which defines what to publish and how - expected snapshot format is ``-`` - Promote publish - use source publish snapshots to create or update another publish (eg. testing -> stable) - Cleanup unused snapshots - Purge publishes and repositories - Restore and dump publishes - Supports Python 3 (recommended) and Python 2 Create or update publish ~~~~~~~~~~~~~~~~~~~~~~~~ First create configuration file where you define Aptly repositories, mirrors and target distributions for publishing. .. code-block:: yaml mirror: # Ubuntu upstream repository trusty-main: # Base for our main component component: main distributions: - nightly/trusty # Mirrored 3rd party repository aptly: # Merge into main component component: main distributions: - nightly/trusty repo: # Some repository with custom software cloudlab: # Publish as component cloudlab component: cloudlab # Use swift storage named myswift for publish storage storage: swift:myswift distributions: # We want to publish our packages (that can't break anything for # sure) immediately to both nightly and testing repositories - nightly/trusty - testing/trusty Configuration above will create two publishes from latest snapshots of defined repositories and mirrors: - ``nightly/trusty`` with component cloudlab and main - creates snapshot ``_main-`` by merging snapshots ``aptly-`` and ``trusty-main-``) - ``testing/trusty`` with component cloudlab, made of repository cloudlab It expects that snapshots are already created (by mirror syncing script or by CI when new package is built) so it does following: - find latest snapshot (by creation date) for each defined mirror and repository - snapshots are recognized by name (eg. ``cloudlab-``, ``trusty-main-``) - create new snapshot by merging snapshots with same publish component - eg. create ``_main-`` from latest ``trusty-main-`` and ``aptly-`` snapshots - merged snapshots are prefixed by ``_`` to avoid collisions with other snapshots - first it checks if merged snapshots already exists and if so, it will skip creation of duplicated snapshot. So it's tries to be fully idempotent. - create or update publish or publishes as defined in configuration It can be executed like this: :: aptly-publisher -c config.yaml -v --url http://localhost:8080 publish Promote publish ~~~~~~~~~~~~~~~ Let's assume you have following prefixes and workflow: - nightly - created by `publish` action when there's new snapshot or synced mirror - packages are always up to date - testing - freezed repository for testing and stabilization - stable - well tested package versions - well controlled update process There can be more publishes under prefix, eg. ``nightly/trusty``, ``nightly/vivid`` Then you need to switch published snapshots from one publish to another one. :: aptly-publisher -v --url http://localhost:8080 \ --source nightly/trusty --target testing/trusty \ publish You can also specify list of components. When you have separate components for your packages (eg. cloudlab) and security (mirror of trusty security repository), you may need to release them faster. :: aptly-publisher -v --url http://localhost:8080 \ --source nightly/trusty --target testing/trusty \ --components cloudlab security -- publish Finally you are also able to promote selected packages, eg. :: aptly-publisher -v --url http://localhost:8080 \ --source nightly/trusty --target testing/trusty \ --packages python-aptly aptly -- publish Show differences between publishes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can see differences between publishes with following command: :: aptly-publisher -v --url http://localhost:8080 \ --source nightly/trusty --target testing/trusty \ promote --diff Example output can look like this: .. image:: ./doc/publisher_diff_example.png :align: center Cleanup unused snapshots ~~~~~~~~~~~~~~~~~~~~~~~~ When you are creating snapshots regularly, you need to delete old ones that are not used by any publish. It's wise to call such action every time when publish is updated (eg. nightly). :: aptly-publisher -v --url http://localhost:8080 cleanup Purge unused packages from repo and publishes ~~~~~~~~~~~~~~~~~~~~~~~~ When you are uploading a lot version of the same package, you may want to get rid of old packages version in your snapshots. Be careful, the option ``--hard`` will remove the packages from your repos. :: aptly-publisher -v --url http://localhost:8080 --component extra --hard purge Installation ============ You can install directly using from local checkout or from pip: :: python3 setup.py install pip3 install python-aptly Or better build Debian package with eg.: :: dpkg-buildpackage -uc -us Read more ========= For usage informations, see ``aptly-publisher --help`` or generate and view man page. :: PYTHONPATH=. help2man -n "aptly-publisher - tool for easy creation of Aptly multi component publishes" --version-string=$(grep version setup.py|cut -d '"' -f 2) "python3 aptly/publisher/__main__.py" | sed -e s,__main__.py,aptly-publisher,g -e s,__MAIN__.PY,APTLY-PUBLISHER,g > aptly-publisher.1 man aptly-publisher.1 Also see ``doc/examples`` directory. For examples of jenkins jobs, have a look at `tcpcloud/jenkins-jobs `_ repository. Known issues ============ - determine source snapshots correctly (`#271 `_) - cleanup merged snapshots before cleaning up source ones - before that it's needed to run cleanup action multiple times to get all unused snapshots cleaned python-aptly-0.12.10/aptly/000077500000000000000000000000001322737302700154625ustar00rootroot00000000000000python-aptly-0.12.10/aptly/__init__.py000066400000000000000000000000001322737302700175610ustar00rootroot00000000000000python-aptly-0.12.10/aptly/client.py000066400000000000000000000054001322737302700173110ustar00rootroot00000000000000# -*- coding: utf-8 -*- import requests import json import logging from aptly.exceptions import AptlyException lg = logging.getLogger(__name__) class Aptly(object): def __init__(self, url, auth=None, timeout=300, dry=False): self.url = '%s%s' % (url, '/api') self.timeout = timeout self.dry = dry self.session = requests.Session() if auth is not None: self.session.auth = auth self.session.headers.update({ 'Accept': 'application/json', 'Content-type': 'application/json', }) self.api_version = self.get_version() def get_version(self): return self.do_get('/version')["Version"] def _process_result(self, res): if res.status_code < 200 or res.status_code >= 300: raise AptlyException( res, "Something went wrong: %s (%s)" % (res.reason, res.status_code) ) try: return res.json() except ValueError: return res.text def do_get(self, uri, kwargs=None, timeout=None): url = '%s%s' % (self.url, uri) lg.debug("GET %s, args=%s" % (url, kwargs)) res = self.session.get( url, timeout=timeout or self.timeout, params=kwargs, ) return self._process_result(res) def do_post(self, uri, data, timeout=None): data_json = json.dumps(data) url = '%s%s' % (self.url, uri) lg.debug("POST %s, data=%s" % (url, data_json)) if self.dry: return res = self.session.post( url, timeout=timeout or self.timeout, data=data_json, ) return self._process_result(res) def do_delete(self, uri, data=None, timeout=None): data_json = json.dumps(data) if data else "" url = '%s%s' % (self.url, uri) if data: lg.debug("DELETE %s, data=%s" % (url, data_json)) else: lg.debug("DELETE %s" % url) if self.dry: return if data: res = self.session.delete( url, data=data_json, timeout=timeout or self.timeout, ) else: res = self.session.delete( url, timeout=timeout or self.timeout, ) return self._process_result(res) def do_put(self, uri, data, timeout=None): data_json = json.dumps(data) url = '%s%s' % (self.url, uri) lg.debug("PUT %s, data=%s" % (url, data_json)) if self.dry: return res = self.session.put( url, timeout=timeout or self.timeout, data=data_json, ) return self._process_result(res) python-aptly-0.12.10/aptly/decorators.py000066400000000000000000000012511322737302700202000ustar00rootroot00000000000000# -*- coding: utf-8 -*- class CachedMethod(object): """ Decorator for caching of function results """ def __init__(self, function): self.function = function self.mem = {} def __call__(self, *args, **kwargs): cached = kwargs.pop('cached', True) if cached is True: if (args, str(kwargs)) in self.mem: return self.mem[args, str(kwargs)] tmp = self.function(*args, **kwargs) self.mem[args, str(kwargs)] = tmp return tmp def __get__(self, obj, objtype): """ Support instance methods """ import functools return functools.partial(self.__call__, obj) python-aptly-0.12.10/aptly/exceptions.py000066400000000000000000000003051322737302700202130ustar00rootroot00000000000000# -*- coding: utf-8 -*- class AptlyException(Exception): def __init__(self, res, msg): Exception.__init__(self, msg) self.res = res class NoSuchPublish(Exception): pass python-aptly-0.12.10/aptly/publisher/000077500000000000000000000000001322737302700174575ustar00rootroot00000000000000python-aptly-0.12.10/aptly/publisher/__init__.py000066400000000000000000000711301322737302700215720ustar00rootroot00000000000000# -*- coding: utf-8 -*- import time import re import logging import yaml import apt_pkg from aptly.exceptions import AptlyException, NoSuchPublish from aptly.decorators import CachedMethod lg = logging.getLogger(__name__) def load_publish(publish): with open(publish, 'r') as publish_file: return yaml.load(publish_file) class PublishManager(object): """ Manage multiple publishes """ def __init__(self, client, storage=""): self.client = client self._publishes = {} self.storage = storage self.timestamp = int(time.time()) def publish(self, distribution, storage=""): """ Get or create publish """ try: return self._publishes[distribution] except KeyError: self._publishes[distribution] = Publish(self.client, distribution, timestamp=self.timestamp, storage=(storage or self.storage)) return self._publishes[distribution] def add(self, snapshot, distributions, component='main', storage=""): """ Add mirror or repo to publish """ for dist in distributions: self.publish(dist, storage=storage).add(snapshot, component) def restore_publish(self, components, restore_file, recreate): publish_file = load_publish(restore_file) publish_source = Publish(self.client, publish_file.get('publish'), storage=publish_file.get('storage', self.storage)) publish_source.restore_publish(publish_file, components=components, recreate=recreate) def dump_publishes(self, publishes_to_save, dump_dir, prefix): if len(dump_dir) > 1 and dump_dir[-1] == '/': dump_dir = dump_dir[:-1] save_list = [] save_all = True if publishes_to_save and not ('all' in publishes_to_save): save_all = False re_publish = None if len(publishes_to_save) == 1 and re.search(r'\(.*\)', publishes_to_save[0]): re_publish = re.compile(publishes_to_save[0]) publishes = self.client.do_get('/publish') for publish in publishes: name = "{}{}{}".format(publish['Storage']+":" if publish['Storage'] else "", publish['Prefix']+"/" if publish['Prefix'] else "", publish['Distribution']) if not re_publish or re_publish.match(name): if save_all or name in publishes_to_save or re_publish: current_publish = Publish(self.client, name, load=True, storage=publish.get('Storage', self.storage)) if current_publish not in save_list: save_list.append(current_publish) if not save_all and not re_publish and len(save_list) != len(publishes_to_save): raise Exception('Publish(es) required not found') for publish in save_list: save_path = ''.join([dump_dir, '/', prefix, publish.name.replace('/', '-'), '.yml']) publish.save_publish(save_path) def _publish_match(self, publish, names=False, name_only=False): """ Check if publish name matches list of names or regex patterns """ if names: for name in names: if not name_only and isinstance(name, re._pattern_type): if re.match(name, publish.name): return True else: operand = name if name_only else [name, './%s' % name] if publish in operand: return True return False else: return True def do_publish(self, *args, **kwargs): try: publish_dist = kwargs.pop('dist') except KeyError: publish_dist = None try: publish_names = kwargs.pop('names') except KeyError: publish_names = None for publish in self._publishes.values(): if self._publish_match(publish.name, publish_names or publish_dist, publish_names): publish.do_publish(*args, **kwargs) else: lg.info("Skipping publish %s not matching publish names" % publish.name) def list_uniq(self, seq): keys = {} for e in seq: keys[e] = 1 return list(keys.keys()) def do_purge(self, config, components=[], hard_purge=False): (repo_dict, publish_dict) = self.get_repo_information(config, self.client, hard_purge, components) publishes = self.client.do_get('/publish') publish_list = [] for publish in publishes: name = '{}/{}'.format(publish['Prefix'].replace("/", "_"), publish['Distribution']) publish_list.append(Publish(self.client, name, load=True)) for publish in publish_list: repo_dict = publish.purge_publish(repo_dict, publish_dict, components, publish=True) if hard_purge: self.remove_unused_packages(repo_dict) self.cleanup_snapshots() @staticmethod def get_repo_information(config, client, fill_repo=False, components=[]): """ fill two dictionnaries : one containing all the packages for every repository and the second one associating to every component of every publish its repository""" repo_dict = {} publish_dict = {} for origin in ['repo', 'mirror']: for name, repo in config.get(origin, {}).items(): if components and repo.get('component') not in components: continue if fill_repo and origin == 'repo': packages = Publish._get_packages("repos", name) repo_dict[name] = packages for distribution in repo.get('distributions'): publish_name = str.join('/', distribution.split('/')[:-1]) publish_dict[(publish_name, repo.get('component'))] = name return (repo_dict, publish_dict) def remove_unused_packages(self, repo_dict): for repo_name, packages in repo_dict.items(): if packages: self.client.do_delete('/repos/%s/packages' % repo_name, data={'PackageRefs': packages}) def cleanup_snapshots(self): snapshots = self.client.do_get('/snapshots', {'sort': 'time'}) exclude = [] # Add currently published snapshots into exclude list publishes = self.client.do_get('/publish') for publish in publishes: exclude.extend( [x['Name'] for x in publish['Sources']] ) # Add last snapshots into exclude list # TODO: ignore snapshots that are source for merged snapshots snapshot_latest = [] for snapshot in snapshots: base_name = snapshot['Name'].split('-')[0] if base_name not in snapshot_latest: snapshot_latest.append(base_name) if snapshot['Name'] not in exclude: lg.debug("Not deleting latest snapshot %s" % snapshot['Name']) exclude.append(snapshot['Name']) exclude = self.list_uniq(exclude) for snapshot in snapshots: if snapshot['Name'] not in exclude: lg.info("Deleting snapshot %s" % snapshot['Name']) try: self.client.do_delete('/snapshots/%s' % snapshot['Name']) except AptlyException as e: if e.res.status_code == 409: lg.warning("Snapshot %s is being used, can't delete" % snapshot['Name']) else: raise class Publish(object): """ Single publish object """ def __init__(self, client, distribution, timestamp=None, recreate=False, load=False, merge_prefix='_', storage="", architectures=[]): self.client = client self.recreate = recreate self.architectures = architectures # Try to get storage from distribution (eg. s3:mys3:xenial) dist_split = distribution.split(':') if len(dist_split) > 1: self.storage = "{}:{}".format(dist_split[0], dist_split[1]) distribution = dist_split[-1] else: self.storage = storage dist_split = distribution.split('/') self.distribution = dist_split[-1] if dist_split[0] != self.distribution: self.prefix = "_".join(dist_split[:-1]) else: self.prefix = '' self.name = '%s/%s' % (self.prefix or '.', self.distribution) self.full_name = "{}{}{}".format(self.storage+":" if self.storage else "", self.prefix+"/" if self.prefix else "", self.distribution) if not timestamp: self.timestamp = int(time.time()) else: self.timestamp = timestamp self.merge_prefix = merge_prefix self.components = {} self.publish_snapshots = [] if load: # Load information from remote immediately self.load() def __eq__(self, other): if not isinstance(other, Publish): return False diff, equal = self.compare(other) if not diff: return True def __ne__(self, other): return not self.__eq__(other) def compare(self, other, components=[]): """ Compare two publishes It expects that other publish is same or older than this one Return tuple (diff, equal) of dict {'component': ['snapshot']} """ lg.debug("Comparing publish %s (%s) and %s (%s)" % (self.name, self.storage or "local", other.name, other.storage or "local")) diff, equal = ({}, {}) for component, snapshots in self.components.items(): if component not in list(other.components.keys()): # Component is missing in other diff[component] = snapshots continue equal_snapshots = list(set(snapshots).intersection(other.components[component])) if equal_snapshots: lg.debug("Equal snapshots for %s: %s" % (component, equal_snapshots)) equal[component] = equal_snapshots diff_snapshots = list(set(snapshots).difference(other.components[component])) if diff_snapshots: lg.debug("Different snapshots for %s: %s" % (component, diff_snapshots)) diff[component] = diff_snapshots return (diff, equal) @staticmethod @CachedMethod def _get_packages(client, source_type, source_name): return client.do_get('/{}/{}/packages'.format(source_type, source_name)) @staticmethod @CachedMethod def _get_publishes(client): return client.do_get('/publish') @staticmethod @CachedMethod def _get_snapshots(client): return client.do_get('/snapshots', {'sort': 'time'}) def _get_publish(self): """ Find this publish on remote """ publishes = self._get_publishes(self.client) for publish in publishes: if publish['Distribution'] == self.distribution and \ publish['Prefix'].replace("/", "_") == (self.prefix or '.') and \ publish['Storage'] == self.storage: return publish raise NoSuchPublish("Publish %s (%s) does not exist" % (self.name, self.storage or "local")) def _remove_snapshots(self, snapshots): for snapshot in snapshots: self.client.do_delete('/snapshots/%s' % snapshot) def save_publish(self, save_path): """ Serialize publish in YAML """ timestamp = time.strftime("%Y%m%d%H%M%S") yaml_dict = {} yaml_dict["publish"] = self.name yaml_dict["name"] = timestamp yaml_dict["components"] = [] yaml_dict["storage"] = self.storage for component, snapshots in self.components.items(): packages = self.get_packages(component) package_dict = [] for package in packages: (arch, name, version, ref) = self.parse_package_ref(package) package_dict.append({'package': name, 'version': version, 'arch': arch, 'ref': ref}) snapshot = self._find_snapshot(snapshots[0]) yaml_dict["components"].append({'component': component, 'snapshot': snapshot['Name'], 'description': snapshot['Description'], 'packages': package_dict}) name = self.name.replace('/', '-') lg.info("Saving publish %s in %s" % (name, save_path)) with open(save_path, 'w') as save_file: yaml.dump(yaml_dict, save_file, default_flow_style=False) def purge_publish(self, repo_dict, publish_dict, components=[], publish=False): apt_pkg.init_system() new_publish_snapshots = [] for snapshot in self.publish_snapshots: # packages to be kept processed = [] name = snapshot["Name"] component = snapshot["Component"] purge_packages = [] location = self.name.split('/')[0].replace('_', '/') if (location, component) in publish_dict: repo_name = publish_dict[(location, component)] else: new_publish_snapshots.append(snapshot) continue if components and component not in components: new_publish_snapshots.append(snapshot) if repo_dict: repo_dict[repo_name] = [] continue packages = self._get_packages(self.client, "snapshots", name) packages = sorted(packages, key=lambda x: self.parse_package_ref(x)[2], reverse=True, cmp=apt_pkg.version_compare) for package in packages: package_name = self.parse_package_ref(package)[1] if package_name not in processed: processed.append(package_name) if repo_dict and repo_name in repo_dict and package in repo_dict[repo_name]: repo_dict[repo_name].remove(package) purge_packages.append(package) if purge_packages != packages: snapshot_name = '{}-{}'.format(name, 'purged') try: lg.debug("Creating new snapshot: %s" % snapshot_name) self.client.do_post( '/snapshots', data={ 'Name': snapshot_name, 'SourceSnapshots': [], 'Description': 'Minimal snapshot from {}'.format(repo_name), 'PackageRefs': purge_packages, } ) except AptlyException as e: if e.res.status_code == 404: raise Exception('Error while creating snapshot : {}'.format(repr(e))) else: lg.debug("Snapshot %s already exist" % snapshot_name) new_publish_snapshots.append({ 'Component': component, 'Name': snapshot_name }) else: new_publish_snapshots.append(snapshot) if self.publish_snapshots != new_publish_snapshots: self.publish_snapshots = new_publish_snapshots if publish: self.do_publish(recreate=False, merge_snapshots=False) return repo_dict def restore_publish(self, config, components, recreate=False): """ Restore publish from config file """ if "all" in components: components = [] try: self.load() publish = True except NoSuchPublish: publish = False new_publish_snapshots = [] to_publish = [] created_snapshots = [] for saved_component in config.get('components', []): component_name = saved_component.get('component') if not component_name: raise Exception("Corrupted file") if components and component_name not in components: continue saved_packages = [] if not saved_component.get('packages'): raise Exception("Component %s is empty" % component_name) for package in saved_component.get('packages'): package_ref = '{} {} {} {}'.format(package.get('arch'), package.get('package'), package.get('version'), package.get('ref')) saved_packages.append(package_ref) to_publish.append(component_name) snapshot_name = '{}-{}'.format("restored", saved_component.get('snapshot')) lg.debug("Creating snapshot %s for component %s of packages: %s" % (snapshot_name, component_name, saved_packages)) try: self.client.do_post( '/snapshots', data={ 'Name': snapshot_name, 'SourceSnapshots': [], 'Description': saved_component.get('description'), 'PackageRefs': saved_packages, } ) created_snapshots.append(snapshot_name) except AptlyException as e: if e.res.status_code == 404: # delete all the previously created # snapshots because the file is corrupted self._remove_snapshots(created_snapshots) raise Exception("Source snapshot or packages don't exist") else: raise new_publish_snapshots.append({ 'Component': component_name, 'Name': snapshot_name }) if components: self.publish_snapshots = [x for x in self.publish_snapshots if x['Component'] not in components and x['Component'] not in to_publish] check_components = [x for x in new_publish_snapshots if x['Component'] in components] if len(check_components) != len(components): self._remove_snapshots(created_snapshots) raise Exception("Not possible to find all the components required in the backup file") self.publish_snapshots += new_publish_snapshots self.do_publish(recreate=recreate, merge_snapshots=False) def load(self): """ Load publish info from remote """ publish = self._get_publish() self.architectures = publish['Architectures'] for source in publish['Sources']: component = source['Component'] snapshot = source['Name'] self.publish_snapshots.append({ 'Component': component, 'Name': snapshot }) snapshot_remote = self._find_snapshot(snapshot) for source in self._get_source_snapshots(snapshot_remote, fallback_self=True): self.add(source, component) def get_packages(self, component=None, components=[], packages=None): """ Return package refs for given components """ if component: components = [component] package_refs = [] for snapshot in self.publish_snapshots: if component and snapshot['Component'] not in components: # We don't want packages for this component continue component_refs = self._get_packages(self.client, "snapshots", snapshot['Name']) if packages: # Filter package names for ref in component_refs: if self.parse_package_ref(ref)[1] in packages: package_refs.append(ref) else: package_refs.extend(component_refs) return package_refs def parse_package_ref(self, ref): """ Return tuple of architecture, package_name, version, id """ if not ref: return None parsed = re.match('(.*)\ (.*)\ (.*)\ (.*)', ref) return parsed.groups() def add(self, snapshot, component='main'): """ Add snapshot of component to publish """ try: self.components[component].append(snapshot) except KeyError: self.components[component] = [snapshot] def _find_snapshot(self, name): """ Find snapshot on remote by name or regular expression """ remote_snapshots = self._get_snapshots(self.client) for remote in reversed(remote_snapshots): if remote["Name"] == name or \ re.match(name, remote["Name"]): return remote return None def _get_source_snapshots(self, snapshot, fallback_self=False): """ Get list of source snapshot names of given snapshot TODO: we have to decide by description at the moment """ if not snapshot: return [] source_snapshots = re.findall(r"'([\w\d\.-]+)'", snapshot['Description']) if not source_snapshots and fallback_self: source_snapshots = [snapshot['Name']] source_snapshots.sort() return source_snapshots def merge_snapshots(self): """ Create component snapshots by merging other snapshots of same component """ self.publish_snapshots = [] for component, snapshots in self.components.items(): if len(snapshots) <= 1: # Only one snapshot, no need to merge lg.debug("Component %s has only one snapshot %s, not creating merge snapshot" % (component, snapshots)) self.publish_snapshots.append({ 'Component': component, 'Name': snapshots[0] }) continue # Look if merged snapshot doesn't already exist remote_snapshot = self._find_snapshot(r'^%s%s-%s-\d+' % (self.merge_prefix, self.name.replace('./', '').replace('/', '-'), component)) if remote_snapshot: source_snapshots = self._get_source_snapshots(remote_snapshot) # Check if latest merged snapshot has same source snapshots like us snapshots_want = list(snapshots) snapshots_want.sort() lg.debug("Comparing snapshots: snapshot_name=%s, snapshot_sources=%s, wanted_sources=%s" % (remote_snapshot['Name'], source_snapshots, snapshots_want)) if snapshots_want == source_snapshots: lg.info("Remote merge snapshot already exists: %s (%s)" % (remote_snapshot['Name'], source_snapshots)) self.publish_snapshots.append({ 'Component': component, 'Name': remote_snapshot['Name'] }) continue snapshot_name = '%s%s-%s-%s' % (self.merge_prefix, self.name.replace('./', '').replace('/', '-'), component, self.timestamp) lg.info("Creating merge snapshot %s for component %s of snapshots %s" % (snapshot_name, component, snapshots)) package_refs = [] for snapshot in snapshots: # Get package refs from each snapshot packages = self._get_packages(self.client, "snapshots", snapshot) package_refs.extend(packages) try: self.client.do_post( '/snapshots', data={ 'Name': snapshot_name, 'SourceSnapshots': snapshots, 'Description': "Merged from sources: %s" % ', '.join("'%s'" % snap for snap in snapshots), 'PackageRefs': package_refs, } ) except AptlyException as e: if e.res.status_code == 400: lg.warning("Error creating snapshot %s, assuming it already exists" % snapshot_name) else: raise self.publish_snapshots.append({ 'Component': component, 'Name': snapshot_name }) def drop_publish(self): lg.info("Deleting publish, distribution=%s, storage=%s" % (self.name, self.storage or "local")) self.client.do_delete('/publish/%s' % (self.full_name)) def update_publish(self, force_overwrite=False, publish_contents=False, acquire_by_hash=True): lg.info("Updating publish, distribution=%s storage=%s snapshots=%s" % (self.name, self.storage or "local", self.publish_snapshots)) self.client.do_put( '/publish/%s' % (self.full_name), { 'Snapshots': self.publish_snapshots, 'ForceOverwrite': force_overwrite, 'SkipContents': not publish_contents, 'AcquireByHash': acquire_by_hash, } ) def create_publish(self, force_overwrite=False, publish_contents=False, architectures=None, acquire_by_hash=True): lg.info("Creating new publish, distribution=%s storage=%s snapshots=%s, architectures=%s" % (self.name, self.storage or "local", self.publish_snapshots, architectures)) if self.prefix: prefix = '%s%s' % ("/"+self.storage+":" or "/", self.prefix) else: prefix = '%s' % ("/"+self.storage+":" or "") opts = { "Storage": self.storage, "SourceKind": "snapshot", "Distribution": self.distribution, "Sources": self.publish_snapshots, "ForceOverwrite": force_overwrite, 'SkipContents': not publish_contents, 'AcquireByHash': acquire_by_hash, } if architectures or self.architectures: opts['Architectures'] = architectures or self.architectures self.client.do_post( '/publish%s' % (prefix or ''), opts ) def do_publish(self, recreate=False, no_recreate=False, force_overwrite=False, publish_contents=False, acquire_by_hash=False, architectures=None, merge_snapshots=True, only_latest=False, config=None, components=[]): if merge_snapshots: self.merge_snapshots() try: publish = self._get_publish() except NoSuchPublish: publish = False if only_latest: (_, publish_dict) = PublishManager.get_repo_information(config, self.client) self.purge_publish([], publish_dict, components, False) if not publish: # New publish self.create_publish(force_overwrite, publish_contents, architectures or self.architectures, acquire_by_hash) else: # Test if publish is up to date to_publish = [x['Name'] for x in self.publish_snapshots] published = [x['Name'] for x in publish['Sources']] to_publish.sort() published.sort() if recreate: lg.info("Recreating publish %s (%s)" % (self.name, self.storage or "local")) self.drop_publish() self.create_publish(force_overwrite, publish_contents, architectures or self.architectures, acquire_by_hash) elif to_publish == published: lg.info("Publish %s (%s) is up to date" % (self.name, self.storage or "local")) else: try: self.update_publish(force_overwrite, publish_contents, acquire_by_hash) except AptlyException as e: if e.res.status_code == 404: # Publish exists but we are going to add some new # components. Unfortunately only way is to recreate it if no_recreate: lg.error("Cannot update publish %s (adding new components?), falling back to recreating it is disabled so skipping." % self.full_name) else: lg.warning("Cannot update publish %s (adding new components?), falling back to recreating it" % self.full_name) self.drop_publish() self.create_publish(force_overwrite, publish_contents, architectures or self.architectures) else: raise python-aptly-0.12.10/aptly/publisher/__main__.py000066400000000000000000000377131322737302700215640ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import print_function import sys import argparse from aptly.client import Aptly from aptly.publisher import PublishManager, Publish from aptly.exceptions import NoSuchPublish import yaml import logging import copy import re logging.basicConfig() lg_aptly = logging.getLogger('aptly') lg = logging.getLogger('aptly-publisher') def load_config(config): with open(config, 'r') as fh: return yaml.load(fh) def get_latest_snapshot(snapshots, name): for snapshot in reversed(snapshots): if re.match(r'%s-\d+' % name, snapshot['Name']): return snapshot['Name'] def main(): parser = argparse.ArgumentParser("aptly-publisher") group_common = parser.add_argument_group("Common") parser.add_argument('action', help="Action to perform (publish, promote, cleanup, restore, dump, purge)") group_common.add_argument('-v', '--verbose', action="store_true") group_common.add_argument('-d', '--debug', action="store_true") group_common.add_argument('--dry', '--dry-run', action="store_true") group_common.add_argument('--timeout', type=int, default=300, help="Aptly client timeout. Raise for larger publishes and slow server.") group_common.add_argument('--url', required=True, help="URL to Aptly API, eg. http://localhost:8080") group_common.add_argument('--recreate', action="store_true", help="Drop publish and create it again, only way to add new components") group_common.add_argument('--no-recreate', action="store_true", help="Never recreate publish (even when we are adding new components where it's the only option)") group_common.add_argument('--force-overwrite', action="store_true", help="Overwrite files in pool/ directory without notice") group_common.add_argument('--publish-contents', action="store_true", default=False, help="Publish contents. It's slow so disabled by default to support large repositories.") group_common.add_argument('--acquire-by-hash', action="store_true", default=False, help="Use Acquire-by-hash option. This may help with repository consistency.") group_common.add_argument('--components', nargs='+', help="Space-separated list of components to promote or restore or to purge (in case of purge)") group_common.add_argument('--storage', default="", help="Storage backend to use for all publishes, can be empty (filesystem, default), swift:[name] or s3:[name]") group_common.add_argument('-p', '--publish', nargs='+', help="Space-separated list of publish") group_publish = parser.add_argument_group("Action 'publish'") group_publish.add_argument('-c', '--config', default="/etc/aptly/publisher.yaml", help="Configuration YAML file") group_publish.add_argument('--dists', nargs='+', help="Space-separated list of distribution to work with (including prefix), default all.") group_publish.add_argument('--architectures', nargs='+', help="List of architectures to publish (also determined by config, defaults to amd64, i386)") group_publish.add_argument('--only-latest', action="store_true", default=False, help="Publish only latest packages of every publishes") group_promote = parser.add_argument_group("Action 'promote'") group_promote.add_argument('--source', help="Source publish to take snapshots from. Can be regular expression, eg. jessie(/?.*)/nightly") group_promote.add_argument('--target', help="Target publish to update. Must be format if source is regex, eg. jessie{0}/testing") group_promote.add_argument('--packages', nargs='+', help="Space-separated list of packages to promote") group_promote.add_argument('--diff', action="store_true", help="Show differences between publishes (snapshots to be updated)") group_purge = parser.add_argument_group("Purge") group_purge.add_argument('--hard', action="store_true", default=False, help="Remove all unused packages and snapshots") group_restore = parser.add_argument_group("Action 'restore'") group_restore.add_argument('-r', '--restore-file', help="File used to restore publish") group_save = parser.add_argument_group("Action 'dump'") group_save.add_argument('-s', '--save-dir', default='.', help="Path of where dump of publish will be done") group_save.add_argument('-x', '--prefix', default="saved-", help="Prefix for dump files' names") args = parser.parse_args() if args.verbose: lg_aptly.setLevel(logging.INFO) lg.setLevel(logging.INFO) if args.debug: lg_aptly.setLevel(logging.DEBUG) lg.setLevel(logging.DEBUG) client = Aptly(args.url, dry=args.dry, timeout=args.timeout) publishmgr = PublishManager(client, storage=args.storage) if args.action == 'publish': action_publish(client, publishmgr, config_file=args.config, recreate=args.recreate, no_recreate=args.no_recreate, force_overwrite=args.force_overwrite, publish_contents=args.publish_contents, acquire_by_hash=args.acquire_by_hash, publish_names=args.publish, publish_dist=args.dists, architectures=args.architectures, only_latest=args.only_latest, components=args.components) elif args.action == 'promote': if not args.source or not args.target: parser.error("Action 'promote' requires both --source and --target arguments") action_promote(client, source=args.source, target=args.target, components=args.components, recreate=args.recreate, no_recreate=args.no_recreate, packages=args.packages, diff=args.diff, force_overwrite=args.force_overwrite, publish_contents=args.publish_contents, acquire_by_hash=args.acquire_by_hash, storage=args.storage) elif args.action == 'cleanup': publishmgr.cleanup_snapshots() sys.exit(0) elif args.action == 'dump': action_dump(publishmgr, args.save_dir, args.publish, args.prefix) elif args.action == 'purge': config = load_config(args.config) publishmgr.do_purge(config, components=args.components, hard_purge=args.hard) elif args.action == "restore": action_restore(publishmgr, components=args.components, recreate=args.recreate, restore_file=args.restore_file) def promote(client, source, target, components=None, recreate=False, no_recreate=False, packages=None, diff=False, force_overwrite=False, publish_contents=False, acquire_by_hash=False, storage=""): try: publish_source = Publish(client, source, load=True, storage=storage) except NoSuchPublish as e: lg.error(e) sys.exit(1) publish_target = Publish(client, target, storage=storage) try: publish_target.load() except NoSuchPublish: if diff: lg.error("Target publish %s does not exist" % target) sys.exit(1) # Target doesn't have to exist, it will be created pass if diff: # Only print differences and exit action_diff(source=publish_source, target=publish_target, components=components) sys.exit(0) # Check if target is not already up to date diffs, equals = publish_source.compare(publish_target, components=components) if not diffs: lg.warn("Target {0} is up to date with source publish {1}".format(target, source)) if not recreate: lg.warn("There is nothing to do with target publish {0}".format(target)) sys.exit(0) else: lg.warn("Recreating target publish {0} on your command".format(target)) if packages: # We are only going to promote specific packages packages_promoted = False for component, snapshots in publish_source.components.items(): if components and component not in components: # We don't want to promote this component continue # Get packages to promote package_refs = publish_source.get_packages(component=component, packages=packages) if package_refs: # Create snapshot for selected packages snapshot_name = 'ext_%s-%s' % (component, publish_target.timestamp) lg.debug("Creating snapshot %s for component %s of packages: %s" % (snapshot_name, component, packages)) client.do_post( '/snapshots', data={ 'Name': snapshot_name, 'SourceSnapshots': snapshots, 'Description': "Promoted packages from snapshots %s: %s" % (snapshots, packages), 'PackageRefs': package_refs, } ) publish_target.components[component].append(snapshot_name) packages_promoted = True if not packages_promoted: lg.error("No packages were promoted : are you sure components: %s and packages: %s are valid?" % (components, packages)) sys.exit(1) else: # Publish whole components # Use source publish components structure for creation of target publish if not components: publish_target.components = copy.deepcopy(publish_source.components) else: for component in components: try: publish_target.components[component] = copy.deepcopy(publish_source.components[component]) except KeyError: lg.error("Component %s does not exist") sys.exit(1) publish_target.do_publish(recreate=recreate, no_recreate=no_recreate, force_overwrite=force_overwrite, publish_contents=publish_contents, acquire_by_hash=acquire_by_hash, architectures=publish_source.architectures) def find_publishes(client, source, target): ret = [] if not re.search(r'{[0-9]+}', target): lg.error("Source publish is regular expression but target does not refer any match groups. See help for more info.") sys.exit(1) lg.debug("Looking for source publishes matching regular expression: {0}".format(source)) publishes = Publish._get_publishes(client) re_source = re.compile(source) for publish in publishes: name = "{}{}{}".format(publish['Storage']+":" if publish['Storage'] else "", publish['Prefix']+"/" if publish['Prefix'] else "", publish['Distribution']) match = re_source.match(name) if match: try: target_parsed = target.format(*match.groups()) except IndexError: lg.error("Can't format target publish {0} using groups {1}".format(target, match.groups())) sys.exit(1) ret.append((name, target_parsed)) return ret def action_promote(client, **kwargs): # Determine if source is regular expression with group, in this case, we # will work with multiple publishes if re.search(r'\(.*\)', kwargs['source']): for publish in find_publishes(client, kwargs['source'], kwargs['target']): source = publish[0] target = publish[1] lg.info("Found source publish matching regex, promoting {0} to {1}".format(source, target)) kwargs_copy = kwargs.copy() kwargs_copy['source'] = source kwargs_copy['target'] = target try: promote(client, **kwargs_copy) except SystemExit: pass else: promote(client, **kwargs) def action_dump(publishmgr, path, publish_to_save, prefix): publishmgr.dump_publishes(publish_to_save, path, prefix) def action_restore(publishmgr, components, restore_file, recreate): publishmgr.restore_publish(components, restore_file, recreate) def action_diff(source, target, components=[], packages=True): diff, equal = source.compare(target, components=components) if not diff: print("Target {0} is up to date with source publish {1}".format(target.full_name.replace('_', '/'), source.full_name.replace('_', '/'))) return print("\033[1;36m= Differencies per component\033[m") for component, snapshots in diff.items(): if not snapshots: continue published_source = [i for i in source.publish_snapshots if i['Component'] == component][0]['Name'] published_target = [i for i in target.publish_snapshots if i['Component'] == component][0]['Name'] print("\033[1;33m== %s \033[1;30m(%s -> %s)\033[m" % (component, published_target, published_source)) print("\033[1;35m=== Snapshots:\033[m") for snapshot in snapshots: print(" - %s" % snapshot) if packages: print("\033[1;35m=== Packages:\033[m") diff_packages = source.client.do_get('/snapshots/%s/diff/%s' % (published_source, published_target)) if not diff_packages: print("\033[1;31m - Snapshots contain same packages\033[m") for pkg in diff_packages: left = source.parse_package_ref(pkg['Left']) right = target.parse_package_ref(pkg['Right']) if not left: # Parse pkg name from target if not in source # This should not happen and is mostly caused by this bug: # https://github.com/smira/aptly/issues/287 pkg_name = right[1] else: pkg_name = left[1] if left: new = left[2] else: new = pkg['Left'] if right: old = right[2] else: old = pkg['Right'] print(' - %s \033[1;30m(%s -> %s)\033[m' % (pkg_name, old, new)) print() def action_publish(client, publishmgr, config_file, recreate=False, no_recreate=False, force_overwrite=False, publish_contents=False, acquire_by_hash=False, publish_dist=None, publish_names=None, architectures=None, only_latest=False, components=[]): if not architectures: architectures = [] snapshots = Publish._get_snapshots(client) config = load_config(config_file) for name, repo in config.get('mirror', {}).items(): snapshot = get_latest_snapshot(snapshots, name) if not snapshot: continue publishmgr.add( component=repo.get('component', 'main'), distributions=repo['distributions'], storage=repo.get('storage', ""), snapshot=snapshot ) for arch in repo.get('architectures', []): if arch not in architectures: architectures.append(arch) for name, repo in config.get('repo', {}).items(): snapshot = get_latest_snapshot(snapshots, name) if not snapshot: continue publishmgr.add( component=repo.get('component', 'main'), distributions=repo['distributions'], storage=repo.get('storage', ""), snapshot=snapshot ) for arch in repo.get('architectures', []): if arch not in architectures: architectures.append(arch) publishmgr.do_publish(recreate=recreate, no_recreate=no_recreate, force_overwrite=force_overwrite, acquire_by_hash=acquire_by_hash, publish_contents=publish_contents, dist=publish_dist, names=publish_names, architectures=architectures, only_latest=only_latest, config=config, components=components) if __name__ == '__main__': main() python-aptly-0.12.10/doc/000077500000000000000000000000001322737302700150765ustar00rootroot00000000000000python-aptly-0.12.10/doc/aptly-publisher.png000066400000000000000000001502141322737302700207330ustar00rootroot00000000000000PNG  IHDR k{bKGD IDATxw\@AA#"ý~UUq!(8Ղ[{uZցQp/B $?r$|ޯ^5羗|{(DBCQ!GOؠ4 e&QF:uJal'O5jQ@e8jPp*g!QLxL$d y@Ws9{f'A qsYp8z5Z,t(g@%rYPb(g@%rYPb(g@J> U`jK |ϖW_N߬ ͓u=?.FgÖȂr V9xS/%@*gї6G^DefF~OWM=_tHpڕm^\Zʞ%ֆF%42JU%o6cW[nҖmҁ;YbBkݲ:my}<ɕlnh3o։KR8¡g3ws@DJ6^׳ﺴQ+Ɩlח_ }EWd 06 te1!#?=_4BRWTqR@J *]Bcl,')N+hfaOEu~VrB7%2.;M圾eMd!6eppÓN{($#V/Պ_>xqe Qy֦/ޥ'sR>>}{o[=fCpd>u!n^4W)Vg>㮐s~0:TNŗsl2P1.1Q}*=EکEۡc VO%9MM0&ZCq+~>.9fs'4;Ϲp&:crLiw0{lt>![]>ի7\n7,-= iи 8wnބE(yN]!"06 Fqmf6+ _ɜIvj@RK9Vi)Og/QY)ҷ/x~Q{9җ5[ %]PB!DgiN/aԞ򾘐bAr}4167K*&kG[40Lj05BS7k.FkW_Ïr3/ ݿ)1`]?èjU>U*c .Pp,7SlX{i@QO;1uzoz6JK9FIm]Bc,էzlG;/뻌مW6Z_!ĝK(,xo/ypZ{VzaADŽ_ swvw" ͢Hm.:HOS6SxwXt633vtr 3FB)Y%մI/q-9N9!<3=UL.|+ϣMg"~ۗ1Ѻ zsr&hwt훹gq^oYMܺ6tjhH@J#գԚP9@5jc%4 }ɢdݎcЗ4ž+SBö?T{+Ot[i%aHYv!OH5A"ʸtYץ jT^/X}Ik1.A9PG/5ԩSmvrȘ7_9yQ^!Q\\Cf>dC FYg^'z\gK񧪪n626\m 6` |QϾ%g- 5oL pvYh 3}6DedL(Ai3>6o ?;PL EQrz#u z&6c7?EŨJCL6y됢 Zc:|A>JS*7BOq#mM2#*^-Ft$ VWg)9 {^== rY)>#|r M}dKCeGa?vO<|RP|HцJ;[e=B FI (XAWM}ē>n-myF_} Y憶<þfJ7/R"||sdk-շ7{_0E$Hj&<[vئ?Doՙok!ש[^0P>x]ϾƖ0klck~!EZ"qC7ϖg<AVŦ޽31י 'z~y<[̀^#g۪H:Ϥe<ԗvt[Q3>>}{oFGJ#ˆ lg^\YBZŦ[=fCpd>!I!n^4M8aҵ)M݈IۯlsY̴TH7^'u|̛s=)*})0*Y!z僫;o*S#jۗq$v#5#lev~‘ Sg1bZclWXO @Yʽѧը!'P0MަFD̴ǤUS-Bn+w ӞW'Y]HQ&ZYyP4ȵ3{^71kťe[Y&oP;oZw5E̩`PTΑ)%i [NOswS?}Vme8pکk6T/,3*3C5rA6PupdE8JI&F2oŷb"θ?iۨϓxfǍr3s- =r+ ^c¯p4mx%|vF#j9߷؝‚Ň*f E, W.Hw냲',]ᾥ ná3%P)f %Rmفb$,6"TM51Cl=g䇌kqhUGVڃYh&l]w{u|o5dߺ߽ҵDep巫#CY˖rz2zj2ʵK#S Kwhu}f-c-?VKh݅o=TGW9hB4L5ʼnY-iT_߸Vl185ʪ au1֕rXxZ:Fa6e?阻ܞЅoy̐vZB +C9΢@CP16tjhX9*iѵo枱{-NeA7jԚP9HK9 E:8"\}‘kF2&/t$h?_y]XyeTY#C~!e3+.wyji:^,}Kk߿|pI-4>mˏݚq7Kj>ѾLE>>d PrjKג_9osgek6+'O5j"KQ!$33J+x<[B?UU|(ʍ=`DuҬyveh3P~U9:QwSF쑑%4]nZ2yaՌǟw|ݮT |X[M.e ղP/MG(Q9[@VyN$ CjF83P1ݠPC9 J ,(1PC9 J ,(1T`Px<[CF \N@q(5z0 @*[f !dٲeLRN0ׁkB,Yt utJV9o1\фFRTVEL!@~1?ԁ+WBV\t utYT3&D"F ,UrE@h(b:OrT rTP(YPeH`YP`\GJ ,2fPPP(YPeH`Y@9 ggY(gAY`,(5ʐ, rTP(YPe(gY(gAY`(5ʐ, rTP(YPetP賳H` YP; J ,2 6fPʐ, rTP΂`Pj(gAfPʐ, ,EQEI$8,0 ,x9Kp~q÷B9 cgAU, rTY`Y@9 >0 ,( 6rTP(YPerr@P΂*P0f)D ΂RC9 >0'hYPe8; C9 PP΂*C9 C9 J @~9Kmp0C9 PT q6N8; C9 PK9s}`YP lJ ,2ơo(gAơo(gAơo(gA,0,(rT}`YrTP7PP΂R`Pj(gAơo(gA,0,@}C9 QP7X,EQ€)O(gYPj(gA!Y8; PT}E)V3^R\X&V;|6666''GqAZiJlFFF||ɓ'bXTݹs˦T*g333WZէOCCCfBzp<==&VJ/_zzzzyy0%:99ݽ{lJr6'''  `PNNEPPPYfVl HΝ;׵kקO2%Tûv:rH-f޿|6myxxp\lٲϚ5+11;;Kwwwss ___#uݻ:ƍ\9n:SSS___ GGG[[[OOOKKC?S+H~7;;aÆI$'''DQ5d;;;B>=0770|W>uTmmm:444=z`:FPe6m*~#4k֌pҘgd !ڵ7n!ɩSNe | %Q߾}۶m[##ŋ+2$P;wt] ϟ2d&??<%%E sٱc"Cֈ#Ξ=+]%/@Cj !^^^mڴٳ(Xnd:*2hl8βeˤ+2iVX!Z.@,!d…zzzվx@텗-[47wwmVaê}~ f9k``0uuuΝx@ӭPWW_bEl6GHrjMhT%̟?iӦ&z{{0(}SVXQu"@}pqq4‚xP cǎ4L*۵.\---cjA،;ssѣG346l6ϯp*4@UOĮ^G\T9_C3gnبZ'mٲe=hcюe/ fkk`<ղ1bDK;;/*eWs 0( mN0x(je/1jcqCPF\BfϞmllL1cF͙mŊ__gjcȐ!{& 4gϞLP;#h;u4rH5/g\fqHifԩ&&&nnnL !K.e:Y0i>鸠8zh>N'pg rRJGK84pO^4gy6f g̙hyu"O(ő}$xtC9 J ,(1PC9 J ,(1PC9 J ,!9]Ft$0 J YPb g?<;ێf|>_H$0 J PIC*gK^Nu .vZ}ĈWnw\@U4r6Մv3|'timi־y~[6o=Ēw; !<>|%ׄB?A|K9 B$Փ{[9(GL!D(jD"{pV|V> Snζ[qY,d ($*@ 6a~/nnE=}]Rﰦ!=M_fuuw_]N=xf?'n k/ٸ<a:ͩN;?)?Xw/Ogppn=wŧħH!Y@P HT*R9f6!x 择>efJ:Ǩt[I~!Ƚ_556fŧc_4 IDAT\xO!}.FMnkp.O`dL~S5nϏ=u;iG#.ǨSWBp$0 J PEC*g aۏYuؤNcJHR !v3]6=!D_;|\S:=A^Rv !yޖ톯9\L7^37Ҡk[5K(h,d ($*@% Մዉ{sy[Tzg1I)6w^Y$8==DO2uW?rqB鍳Zr3ѯqjBQDЮ!U1H`2ҰٜͧQqN3w>3ۚ#}q:|}d٥/:QPqȪy;pɹv޶%,6"xV# ﻟQPqytid'㏴!Y@P HT*R9}-Ţing*nˁfj`̀ukceһFw𖎥յzw];s !vνrlof1tŋ:j Fgf:weݴ!N jo Wk)$0 J PE!W|J qԱiWS?}zќ9Ǐ?vXw)"֨1%@Ξ3G0aB]HJ~Oha' }vODSI|"!!‚XtvP1F t$0 J gg@ժ- DŽן{䜋VQS;~!PkoDHL|ۇ@Cyi բg۪-TtBr*ئ)}~ T'p%[O%C$5L/gEGZ^S9]|K>ߒ_M#,M+U˫F5[93мK_v-FxlQё#|r۩ܚ!>edv!)_ÎY}ZH)RT`S<īsdh.g%Y[۔Ѷ:SRtӭDpgQj^td]~M7_xڗ6×?eϱϤ){05<|dh.HnaӁϷrBɹj9A|K9 B ɹj9lˊO%/}k?t vr6GAf٤%W%]՗˔c4vJZߺ_u4i5üv/_"y|Ftjmwږ=j8fvKJ1cv*DRl÷4NvD"{pV|V> RDa/sw-fVt"HR1jae42DVH͐W>Ǫr.Zܰ~KɘRRIڟ'0-;~sQNvŦ˷Wɹj9h9ٙY=]vsdX֖|}ѹBd[ڼiH[K>ߡﴃsj (Ew&ն/-r.Zd3iemQډɎϦHQҥUc[ͿvA+mTN頪]6 ْI=3އF9.keVW^zG'N=xf?[O5z'l '"ɺ1(g@sJ[uقEY5O/HzEu2%|Rr{1eSMBeoߒo䭷3?쉄7m|7ޛ.nNگT~I~ԆIkbOÇ-X9i':UmGlӈ#<RҩvՐ3i"3$f+crN=cNH9UxwoX%xyOƱ+6e ^%7CEG=?^o\9W;,<,b,9ϸK۽Z}# 7Ej&H+V=g&87y@-~1}0tCV\HlJy"IK IYr~!Ƚ_556f\xJrڸňim_ ^y9[~OR[]L{8ߟfog tY="A?Wi[Ý^K^wkN('ͳ~QԩGVLW)ѯR_><^ߨpnGП7>-?jT[iOWrǞ̝֣ccک+ҿeBcO]⸬ň11y S͊3M:UՐ3"iRcH͐'+^匕.v͛+Ql VIv7\^.?]yy*Za݆c{7җi7 dfvlLVuqn~+Iv\AMQ܃ɯ422s}s !;1޳V~ɳflɳdlTXV.d`嬚nvbfُ7wϷ罹^HI /)[/u۴7&Mx@ƴE} XGTp+Ӑo:Q_zjE]ۄ}5+{ۤ0muЦU7xU&Mvљr3)5N v,пt8:]3ڮT~KR$;5gnTUӼdo$+Yش,+4WnR B3UއT6hePӥyޖ톯9\9*,#ҞRTSi%[T$"DVfai7+[PC-.eOQ%܃ɯ4.M !&Wv߲3)LLޯ5}=M79*UmҖE&7LOY7c<͛![TП;4Ѥ:/OҋBsM)!U icn1_F9¤ wX~prfo?6!=޼K+j~ l O,*iās~4u4X,"|Ăkfn&5$Sn_5VZ iEyZr3[U#I!"_M\VgUlQ єb3U1R>؟I%2uSW^m;UnJLٔx'{05<~M-sW_ <<!j&?}j={N卪&xidrCdHw/c Y--ZAcG~3ŁU"2 2Yu0os!,mFf|r~5YԤ # zFAK'Hjx_KO{[/֎ b.cL9q|n>jc4]%ُs}mc2Kϯ.ڔيK7'mGvիG*}]F}9fZ/ ^~FAA ӁCY7o KRnY!bc=r`ޑҬbJ63Di"#$g竔c2 Dz.\_;o_o M>yR̬ -Ew&ڗc3t{r4gʤB،):Îe佉c[Uܪ ^eA&;n6p}O!D5.׭YKݡl䐎յzw];s !N joժ,5 Jw&RпwT{{ZP3gM{Ϗa)ڗ6&YodaU3bB7mc[;{n"D~lL`֦۷n[q,B)zr,(u`)//Oo ;ɿkYFgf:weݴ!],h~ʵo[΃GN, -%2ա}O;6-*jNU)KHmGJHI*昬}\ҶsKg{3+^|QڦثCFf֖RאP>SM:hvn%[V#a;87h] ^e 2Íxy~M q_M}4Tԇ\tNmQ^vs.JROgwMo?}zќ9Ǐ?vXw)"uBQ Y8{g=zt„ uUy1UU"O(::!됇ʮBr {5D|z&G˹j5AO9V2ƀ {hU9?VH 4CS@%rYPb(g@%rYPb(g@%V[K>8^9 8,(1J"0C/^hݺ5ӱ-[lݺuZJNN600tR.] ߟ8qb׮]zrԠݻwy6o޼6m0Pr6//O[[[CCC(l*))133KMMzϜ9ҥ]24R :vXnn.!D__y̙Lŵk׶o~钒B'@٩x9igggiit,h'Npvv)..;v,*Zhǃ_NOܹutt TUNN/cǎhB=<<thu@>}Ҿ}{ݻ2k,444BCCGW_ݹsvZLL===7o|c___SSSOO-Z<<44, >}r'NHOQWW?qF`mmL'N4hU@@=ӈ+W899YZZdee;44411ߟ[@jQrٳgv1(ڞ={$ĉ &쐐 &dgg<֭[ F@kҤɘ1c._rJx___ggsΉD"ceжmA?^KK#22ڵkcƌQWWg:@cg p?oa:PVZeff޻wsΕDnnnG?{HDAAAejjĉ3g433c::hk CBB !T[NLL|9ӱuUYH4y#Ghii_|#QjjjHHHPP !,#F 6(0,,l֭7n RkСE1"r9[XXE`:P^zݼysSN6H$2eJHHK /X|ƍSn666LG ok׮oB]]]ϟCcl\\uv  ΦBH#Nuܹ~),H ݽ{G)pƩc ȩ`AgBZf8& >ÇݻᡭMޫe˖L :vطo'NX,:B:+PBgglٲ`ٳgܹX@Arss|@ XX~!.{ܹw/''!!!eܹI&MԴiSfc{ЏkѢٳ5kthS峳KW#t@_?b8p`ƌyyyNNNWFP'ttt\]]/_[,y|Y *|QΝF}\^ZjjYYP)#xzzj)v=s̼aÆLDdAÇQxlX,CCC۷oЮ]A8qᣔUVj͛͛7y˗(l9t56m G'(RϚ!EN[2s'Y46EQagםooЌMQv6T $;_զ(jbRR,GhRٷ뮽8$72ѐEQ,CGȥo$7rĎzEiʫgMQ#CI߿ ԩP(d:ƥr6=f̘k׮U5 rRr5hT^۪U!CIEmݺۻh̘1gϞf_?DE|qPr~RKnU_^wYw7v;Aϗhg S~fʎ f_rw m¢F,.2рq+̐{iISί;ȕ[/Os="}r(|q5&zzzVAH٥`$+oZН[U_TZRS˪΢ץd똷ZW.knЕ'{¼iK\v KxW,RtKiPi77f`&u黅QoҷTLm--[8XZZ=aС,--әP)9Zn+^ ر#,g Zht,Pb1}XmV+k׮]lYqq/R+Q[8տ;,(lFV IDATZ~~ڍw &wVׂg2EM#.-b5jpN| rGp%1 3e{W۷۽^{^;ruJR[z}Ƨ)Ghhh3ܹseW?~חW,|`2C^|}>"F:!}e:wڵk'{]˗/'GqRa%ξڄg7~~ۖCѶ3B N?ĂCshN_t&_{37gBG/+2W 5B̆x-STab !nȥmD"DkEv|[Jcg)#D"Y ꥧ[lIgϞx|8//.N ¨`ܥHJJpႆbrJHvŬYQQQewEr力}HdLǪ3},r9@H4f̘͛+lVD'O QzGtLII ttt`ggG͕2CUM߯7..\W^LGPU]ddݻwtt,P_JJJRSS>}(xl6 ^;@Co߾wBtuu:p@C/"?ؾ};}EQ 9r$n ,g%P(|@}9qℳULLLٝiÆ >>>l6{ ;w.((诿;kkkWWWwwwCCC3iii޵kWRR!DWWwܸq^^^666LxgB!Ӂ@߿?!dΝ ưaB:x a1Y0-!!ǧlO&Mg4}O{M4:tغukNNqG*8vk >}r'N`O?$NgϞOo(l?~g..Џ HMM/D:tjժW^1`w%$$D$ѷf:@HeY@ٳG"L8{/ZhƍdٻwV􆇿'}:,j\b|Le?bhӦ͓'OڶmԳ j5g'=zŋΝ8p #C@6,gq[WPPpaRO… 7o,H̙-j>>>O>-'+WMMM}}}? O9b1a233;9u`z0vXBHppp+m_Chеl׮]-[PEQԧ -4/]+!Z#7gge<x{TXmSJ9Zmy:n͓]|Xmѓ~}]!mY1jE!XR,+BDh]Bݐ'ߴ*4 !Z#(J:k-B9Nq7띔 ƻw;uD,wށBe׮]ۿڮh...g cG޸q`9۹sgBHj,ge(gNϞ= !g:CW;vݒ峈"÷((]fmlaɹ6˴iKY*_RsS9ftPz} <1g1EwD[ uG:p!.CXT\kaeH{cNk^S)f{ĺyW^ Ǜ]/K$9ZM>0 ?Wj($#ƞ{xxDDDXd߾}^U6ydBȜ9s---:myVkƩ`9GȨa'x(}+_0?*@MԣG!zzz K^{+۷bs_4\Ճ%5wѥEcA +iׄUڔݔ4SBz?snmb({Y֖7prDRSqފ$҉ӂN ϑ"֫ ˶=rVxkVێ?lKInJ$Mmi4lùҖ×Ag ݻwne_ T9,,g…bڵR˗/VaaahhhqT4p6v͛7ٺ͚5y.M$LӛxCC@ͭJIW۶m&غ-JNipEdeWf[4_5iiLMOYkЯ4)_ e4:x}jsKx Uڱ0sKEQ2vZ w/s mZhm݇ W+ljf3$%gsÉF}50=74Hv;#Oqڵw}OOO###ggg!elڴiӦMĉ˗nڴivkÈ* j,}[ss,>m G'\\85Cq)ҳ'2(>Y|fр6EQ]wӋeC36EQtWP %ٱ'~6EQT/?J_,r$O&E};1!Hr# Y2tt , 8l?=ū&:Smͪ+7(Jèm9:-XThvɪ6o PaR>OYЧ(sss;F7XtE;[UE>>DTMBUajz-[T70ΌK+-bj*7<x/Y`ɻeܰwy"(@7Ƿiy!e*=u^[S}NKO'&r~F9oYҴAGaMan&z\$c{e5[G2a5.yeIjt@_4'L>=00hׯ__-ukdɫM ivÝ.^ sXM:\wy-SC` wq#-??6Ok k9Qswָy]"5y٨|fSaCS j +W411511qvv; Fز(jSY Gu=VimUPPPAeYA@ζdE ! \χy+ƛO>[Zc8dɒwuu}9׽{FM DؼիW<n#gCٹs'Bh˖-lԗc;=(kΞ=?J^^ÇN[[{Ν}tMYk|ٛAlf~1@a>UioXm\uR{\RԦֺQuJGL氬iRV!T\~! zF޳gY:popï Yt߾}m^ wwnllxOG;I[lн;ܾ-XM]TiJ3}HzLS;jŶu% (:X2a]خ5 hN K,!hmzuSR5suA%ՕhzDA+h?PYYikk[RRҺd^t=ݛk1$l䌦j?|)|wZk[]?a W+\+[bt39#3 9 U'l\+|Eig]#Ŋ0׷;ρ|/ھ}ݻɎ>Qj#ΈPn߇d%0_L+?Q7ml6!g+mp8f"$H r&{B.nMm!ϳ) 0 wnݺOi7 B & V]6uiA;K E--W8ʮ/6h~°m.j"Q/uQRRR,_ ' S֭[Ǐ755%:Nt`MAH1bĈD|SNyxx477ڵf H[wѥ <==='''|ck.o>wI U򥆎CCCu%5ϗnf~}ܯQ$Ql,")88!O_/͛aآEBPЦֽ:8ֲwqI x["11QJJJ.k[kE8QITgHaaa|>̙dgs΍7l@v@A9 DP(x{|$vU#ڞ|pvtY/]8X VHToooGGDŽYY_o>@ߐrS6Yk=H0a?^"d/gײﳾj<,e ì7lmaT$v3;/i aRʚ{);!!!MSS,ٳXlT@HN9.];sZ~&ZE0@𸓳#^4'f|dϵwd}sl\PWܦ&C˒+DǏ#.]Jv^@ц,]*Z$l}?(_`cH0޷YRtn=l: j3k7mhשMcۺW?|%KA1Oߊ`?\.-4e!w>ﱧ?%$$X/'&&ʆ%W>+zc 0 ä [\jF!)Y[<# _Ν&_u0 0Z!bŊsӧ'%%=ztҥP;y5үIH9XRR"%%e``c o%89~X|bި5cUV?[<KL~\] Bxs<{,;;NOM69Z+$}P(400f_>ns* k6zcOalUQSսTa#B+(ҥxŗo]=)((qwwweee5;;4cǎ1LhkTKMOxl6ZRa30 @ruK)w.+`47KzٯW=]zij$3'++dǭSLBi9=\9BUV1va^=$qw)S?^AA!,,*Z@2QQ|! Yi/4IMoHH9ֱߙpJx n䥴|j,&o鯇ZRJ\q\@]B /8 = '..jdg!ɓ6<w޵ _v0f{7iT(?s?ӘhhX/WТjO:=zKM>T.3urǢJSuE_6}'3bX߃+L:KW_.!p߿q WKK) زQ"'}肖< 9xs[c#/3ex+P';HhhaGJkqOElj_=%gIZRփ?*._Rڭ AZ- LTe:<_SQfQjB 7S՞|0X jU$D!7JRtsU<2ZoH{$[P|K`mZkZena37;$mnn.**P(&; ,'&&!d2"B,,,ҔN:- bAK7) ZZU[̲ ͙omh(<@_W^痿ٿAUǡJ^=뫷>9uDĨל|AVqոM(r66I(g|,Yg:u#"Z&Nh"hdm{pI f^CDEy\s$D:Wh{3~Xq#a%N= ^cŵ3k~.}4fmovUǨT:劰D+RŗWoyQ驖~v˛NwY}~9JRq8g 5,Th?DF͠:[yoQkHQΦY? Q됪Hqԏ Kآ0]'B}ώU" ]{ IDATwA>rJ&Bvnܸ1qDuuUr!n߾mggW]]=gΜn73ݡ/j7~v9͹|Wc'1]|TK*eϝʷlP w9/,6[ĝ(RÆ #h/_ -t07c+Ii_p|:4->\.-4bF{g ?gtӥ /uc]6sGTzl84Ez6 ]EX2=bu㛯=禿P%gߍ$,]Cs\uuue4QW\\*##Mv_dgg^r*Z )U)A2VRg*h"׏ߍכFZRV:˹QTk=PF*lN.z~ڀֵ9Zc]}K(ӔikoOܿ&}73G-R{I^cCeeݪs+eA͕O9 #Fg6w_$Xi BBB 455"Qʕ+ϟWTT0 z|xmsfPga'Vҁwk(]Ք7Z_eN70Pr1IW !{]lo'CGJ"}׭+~dշ'VnШr sBνe$Ogg_ǏGpX:tիW tU:rV~D xMi.X|9qG=EG U')Om.m0y?~Ym(Nސ>`,> Yyod˼Z!$e0K&q[ M^CECB _ h#' ~GY%ʌ-,," tҥ_v޾D@QU֘<IaAUfeƭ7 eUEaƬrcSCv~ڿrֿj_˰jYD11sAJkqNLf s%"? y\'- y0\מϟ??...**Ã,{lll>b HB++'*)) B:vu7=zλn :\<.fc:nquQ6Ąxbٳgt:ݝ,ҥKVVVׯ_PQQ!;)Cv?L";D&5>=)kY㸻;ن999vvvd'"mw}Чxvf+))tNj@ *ZkkkPUU%;1]0GggC%;螸Ǜ!*!Cܾ}ֶD@rVN`П 8hܹcgg- ,,G7n`0';Kwcc;w- ,1; MgKPPB[^^,VVVݻwmmm_~Mv"ೈ}9 b L&Y5hH(gA߉:u# 0 ++kĈݛ2eJEEىO$,ŪZZZdg]uQ 9rGN| q-g[f9wƍ۷oԼxQ?kkkh#/gGz*++#;=P΂b1 %; ŋMMM?~lmm -"7%B.q^__ooo& K.M6֭[VVVYYYd=J%;:}!,, E`MEE%==}ܸqO<*--%;%dgdgʲϗ7 5mڴ7o:4++KOODU\\P(_}N';4bY[p8/_h4x?T,#}ӧO'O=h C!%%%44ŋ-Z0x`'333366#; DEE>燸`XDEk``  KNNNdddtttuu5BHVVvƌL&4Zl,#aaa|>Pˊ9 *Zsss-L&s…f (gA/ ǎCpb0.\--- $\SSӅ |>BH[[oԨQd.,gK8|AA Y@V7n w7%yyyǏBHFF:%?&`vV\w Pĵ%\?_=bEFF^vbbbxbMMMrĈX^ _\\3*ʊLR7XrΗ.]8p`VV% >P(ʊ}JvЮf+Wtd1cFVVŋMLLN;wܻc⋅ @ccYX8+ʌ-,,zNOII!*Zkk묬,hArrrBCCcccBrrr8г؊+7$!*ڙ3g^xŋ#F ;EeeeQQQaaaoc-Z@n6rtgϞeggtwwwFӓ]\\233mlljllLOOJHHhnnFxzz:t%~,Ί ݕz^kEAю9Pd> Dh4{{{h w)ы/򌍍XUUu-sss8wsijjfffM۷o[ƱfbV677t@reeeɎ3a„,w566hhh\x*qӧy<BHEE- `رd;bP~~>700ZVd hӧO%''dff Ǐƞ8qid2]\\dddNĬ".77ƍ cdg}AFF&>>h---ǍGv(jkkZ6lK,;H,IAAA!oooyyy>BTKJJ6mThpBJJJ...^^^8 :Ĭ.] L&Y@?~bb]zz>KiiittcLjIc"LYeѵSNFPkE0mڴ .L0P###҈Ʊ0YbYOUtQҥK!--}ԩ ={hKC"dž~!D\]]===T*#ԨK(:X)jnܸ1qDuuFv@@`0}Dؠw[====<<]$NEEE:::Pˊ ?ԲJ0ɓӧOOKK?C D>RUU;weN#N,,Y,+>>0___QԈ â";h8q"""!DRƱf͒&;| (gAwppWT'BvvvΝ$9SqÇ 0tYĩ.]"+,, BDFF:;;CEK Px'Or\9s-gS9 )+++//OOOɉ,@Pp """SRR_LԵDEkggGv(ѣǏz AXrAĝ( Yx0СCD];cƌӧO;;;Ji+Kt1%6 _NR; ]΀<1G ^k!?·NRkގS4$x7^hAƆ;rsѰmp\4A/N냗I$N@xP@@YDٳg϶mgLL qz 7nȑ#+++MLLW\\&% [eҵmF-ҥvmGs m&Տ[\8!3be\a&UI飕7ב#OZ\^+hwQof0~*% |5E\-i:45kv4l[9ϊA#.2Xoq '%,5qqqUUU&L077'; 4wޱc@ :y$qzP("csrrƱӴs*Mi5<حOSJkAmh6qeh iq-o L?[<KL~\] Bs!:X2R*[66ݯt.lm+f?槧Moggv8rΊ;HnΝ;::8ɓ'v244jll$ǖ;drFSR]>;c^U\?a 6ۂfٓtOST ub5a#B+(ҥxŗo n;dy`f!*m7vrʁU8Nmyn;,,]KՍ|nWevTEMW 론L YUUU%//Ev.77ƍ cdgk׮]>>>QQQd6G4>|w}WXX_ܹŋDX999# _ybI'9?3QYtp]{RhsV/uԣ(X-$Т>`oNWPa]qy >~Ϲ#/x QӚ0R!8駤ڱ3utX=?Nm+g=5|ʝo'( 8V)f&O@g 0\߁y F}]w˖- ^j$\``MTjxxq$'''444&& 䜝L IV)^Jj IDATp tL&Y۸q#a7n\d d'jWYYYTTԱcLjV!sss&h"Oc bSB[EGGN:uĈdg·~aط~KT>>>d'zOcccrrrdddZZZss3BHWW$,Y=z!tR~dÆ mذaɒ% "èׯ_#h4C4]Bv$.]ƍoVWWwuu%; _֯_a/_eJRSS|b:YA9 kgEџϏFu3=kݺu[nŊ8/_/](fee&%%555!TTTܖ.]:f̘LMـ())S(bVL"X,]]]_]v-aZbE<Ǐccc?^TTRVVVL&EFF Q}FFFP˒(<<jY@oFNNn+Wq믿MLLx"}||5 1(g4 [XX;tR Ö-[j*W\ك ׯGEEt]bZ/[DQWCC ;;;44411#͛dɒѣGm^=Ogg?NTTP˒FFF666dgmK,Ao۶ m۶=8q˗/BT*֖dΚ5 ~rVX@@4lɒ%rrr۷oq|l6ԩS׮]#1660`i8.gRSSi47Y… 1 ܱcGrrGƱ c&M";#,h[hh@ Xp&YDqqqaaWnݺa8ÃN@rD[CCCJJJhhhhjjjBIL&^ ,Yʌ-,,@rrrV^;o޼L斑QYY+%%fĔ" 2x`;ĝVX"<...<<<77bnnd2.\Hl7oBYhQ`` ぁD'O 6hР|/Ϟ=/JKKɎjjjpBTTTkXmmm777___SS69}E| ~Ǿ O,4 KPPPEFF?~իW!ggg//NΝ;ð O8O}@_rEDD  bŽ8k]o1gΜ9s߿Z^dr'..j„ dgP(ʊwܹw:tѢE>>> JMM={vCCҥKwh@@ 0А,HPPBjYrrrBCCcccBrrrL&Ʀ7jMWW`ǃ!%%%dg/8nmmFAv ʢž>}Jl!.ZHAA… fjhh`2PdYb,\֗kkkN ,qQQQ !OOO__ߡCYӧ'%%͚5+44` E+D%u`}ѣKHFEEWVV"h4 ?M8k֬G8-;.gavܸqꮮdghrmb 8W]]lӦMKKKsrr:v㡡PXrt1?F#; ccO>B***nnncǎ%;[SL?p?z(T DRQF=x ''G~J*..h@\kg|ٳuuuDKMMM|||pp;w-枞jjjf%_}UVV]||}!-Q;`0jkk_~-@"ܹs3f022z t߉'"""***BT*ʊdΚ5to߶3gNlll{ vDyemm2ԲX@@ԲYmmmddI```EEWZZcfff9sյD $+$61Z*VQ0D@\m FQQQjj*F&; P(zj@@wffgFFƣG6n8`3i̘1DEΞ=*Z@֒ӕl$Ru6"AYX87BCC&Y@*)) :tɓCCCy;$#Dfy ve ?]aSG\$+YXf)cfƮZAr) sz$ٲk *a f|%gײ~[z.q/I\o3X0L-tz'#:aQA-~T4f唁&mj)$+Y5%<6)g"?n6CuA & \BvIfmm:|0A@/*//?pѣ[8PUUEv4񓗗>}z}}=qD+Ѳevq(dKZ--1rrqr}UK9ҿZ/Z+%;IRޭ R(i3[ZȜ+Kt1%oJ%Rポ&׸G^coNmr[;|+m|xVVvzz #;hyJ׶_H`7q5Fjӈh:45|BTr}1i9iiR&ALJ_4x)e>2$Jڭ-#'%lzo 0w9e嶺Sq(Cfʸ¦Fh˴|=R[Po?*:aCyo}Fyz;x:~9s_V9ۺp`{x. l6;44tҤIDW^۷6%ÇȰp8d'"2v;hR*[:5ݯtK1 6^Ph8 _e ĕ*xAɫ\'4*TYM_Rn?`ܲ6wWaWs+Q,gA0a9Ygyql@@k ɼrÇ7n-zðaòttt._mM9:p+r`!)*jʼWXRM}\9{UVm%bwiG-Y|$*CovC+x뇾9̝cJo?*wBNCU-.uC3_WwK^9诠_ZFv銋 B4mhh(++ 4i%ܰaòuuu/_mg'[;v'IU~ʿSZ%g=5|ʝo'( 8Ç$i&'ARx:eG~hBHii!==\mWޯPQ;8E9#;o!mH{ϫv%{x{W=|||"""BCC"rssG\RRMFNCCCJJJhhŋoہ.Z( `dw>}jeeUZZ:iҤ+*vbFPra7lY9vՠk $~kLl By{{C-+^rrrBCCcccB3f`2666ʜ,CζzCjj*TKP{?xɆ"]e} `[\Ņݿbnnd2.\(:t+W]b'Y̺fsh-6p8\.BE 88xٲeSNtY@G.\܌vss5j 'NDv"wDhv544Z7=z!tRv=|0***<<!$##*%%Bߪ]VVVׯ_OKKƍoVWWwuu%; Ŋvkɒ%f]a``p%++7nX[[w77\~~~4Zwn9zP(ʊ<}4q)eey1L ,v+rrr222#fq8:6][[[WWWWWrB( JrrrF*ԾONNNAAAZZZYYYJJJIIFtyyy!%%`0dddtL5Dϟ?GDҰXx |}}Bɓ'111! bkk&'+}6Tp8/_|Wjjjjjjjb BPx0=򪪪*o~***;*gav;88%;wܻc⋅ .^t 8}mFF١$P]]]EEׯR([O[?nhh)+++)))(((******++[t:V>xw=677BBf755 677X,>px<^CCC]]]sssMM qlCCp8555\.w (++u >Ԅ2l0po㢨?X`k@Vj}*]ZfN+KML-;f/*5ET@n^ca ˱\;z>zs|f{,((̴Wv[F77䰰eeoO?ObٳgƍcGiӦx{{矨h@ TTT:/e]r\}}}}}}툼7]VVVһ~g%fbbbaaaeeeiiiaaammmaaaiiieeg*J9+ \P(EիWOnee`EEE?w}Gy 7]x12d%%%ӧOONN?"U$srrrsssssrrrkkk{QWWĄ.U[I v.j>|HQRRRRRR^^^\\,9gjٍ`gg%F9Muu'YYYmmm(6lիWc*LSSSxxc---.]j*1J_zuڴiqqq3f?{ܮ QRu566>x ##7+++777??_~V6mee񬭭yӦM^ƍcn9Ԕ5??f_رcYG]]]WWWWW.WWWw)pi|WWW>2vXuuuN0ΝݺuLQڵkw޽yٳgӦMӧO9sO>RZDo߶spp 9s67=tйs皛 !=K/ց(+++Z>UG^__ojjjffň+AJJJRRRrr2onnnʕbٍ8n8[[[% QRR"]থ%''?|KgZZZNNNtuzr666vkGGGCC课ل_~oQJjĉw%888ܹsҥJN>`OKK;uѣG!SN]fMHHn]ݕ IDAT͘1#!!_AӧO/\rΝeI񚓓e{aggGTTT$''uY}|>cDޘKw6prrzoǍŴ_fϞ}%Qee͛CCC+**>|H_fn݊ǃI8qboڰQrXggE666R`3f;;;_z;s !^ϕ@RRRwTBF MMMgggWWW777_È'+$enFFFKKt7ƍS***We,g~?ǯo޼c)ILbŊ~.]Si%.\0ٳgotƱzzz!!!˗/ǍcA~UUU3gΌqrr|@ X[[*xAjkk{ALLL\\\\\\|||iitNNN]AZZZ233] \kkko|||>L?YMM-00pK,q/CC?#88޽{&MkYBH~~۷n@QQQLJo$ū9p7 _X,OZZZ:99Ͽp`˶NN2姟~;wL )e^^^...)))]>~$C+V8|0 J$$$OKﲻ?Õ+WZ[[ !VVVK,Yzr #ٳg'O#(g322޽K4+++l“X, !mmmqqqO\\\lllYY٭[nݺEw"EΝ?_|qa$%?lvDKK+**SYMbC]oG}AAA%%%#G⋄~ɓ5sYl٬Yp]6 P(tӧ/][ _Y[[֭2oy}qqq'˖-[ti/;LW/^Oy(߿q֭k׮яX,OO@zo+&:::"""&&Ν;i 2e%xyy]xJ _رcΝ8<~0<ŋ!'9EQӧO_fMhh(N/ar}h{ByAAsZܹs͛7o޽{Wtkkɓ'N8@F,EEEݾ};"""99Y>Ow ݋KXJ嬤;|U+--χsss|2n'e޼yv/۷G@6))IWN:U򶶶6**?.><П2]4>`@kB !ǧ\Wk7_)O/WDܘӮ֞|mꬃ1mmHv|c>2q7 [qCځ7fwov !gi[߬uS ojjkЭFwDǜ~{dyjW1!2'V +syB O rC~Vȯܐ߁PS9ǎ\DީV3gێ ֯o&-YzMONv|͜x!$b[WL}(;.Ž7Zu`lH嘆[4HJmOwl{}>gwl}|ݻoՇ7(.=ɞR n\VUC9CMmmS"~Ws^'Vj8S>wҜ][O(gT+?w(𔳍i]:#o{>Pl-_n‚]!;\æz[!O͓M5X\ېW;'5K1[3Jm_ګuM=z&uL^}wε>G7b߫tA/Tv;+&jsL'R_grq\?xiV\Mm|[Y>Ų[-?],6rLUj@~;L_!z4*7=K>&V!D|/ڕge4`֊ƎAՎ-5 F3M5e|[`IZN&ت4$4;w!{jui2XOe !]Ҩ:Թ bq-j"Izdik[F5ȯC~ +?wT/ówe`oYx85csu !o}OWkg_F9EGcc'YEŵ$_M(?XNe|Ysjp*ۿm*K/g[!,U>'Zj$KϖSTޞ],m.meT W~p@~+.?SjibWkD?vmj=i[~͹nk_ݪuSVƞ'jkKyJN.+lٿE i'sCz+&7|Fa=XD6!O^q8z_6HajODV4 +G2e6n1v'yOC9g8 C~Ո_~+?wX)vK~Bʛ&j8l~ʒ?4P#?K47>uKOw)I6tן:=ɴ6}ո3dz_>ŝΖV'L.͙͝+7BX>^gya޼F-z4[8 oΌ- =G,:vl7'h {ͻK3N#ȯ85uGKNJ1_A~+@~fdQC9 r ,0Y`0`(gPC9 r ,0Y`0 v0b`>J~W,?+a^)3Qn!Cǎ]`Y)2&mͪ.W[w=~IFZkeYQvU׆ k !h;Z2ɔ5f rA~ p)BL!D9 [,{2t3jh7&U1ڮ:[u]~OiBux:445Ģ!Q`.wt@9\? :jĄPV"RT)zVW\Smn$65'č3[ DR/s!N6P-F{.&7;~ !p&ae=eiO粫b555JAM_B~GU ;DžY^)_qƮ[PVuʰ.\Ya;š"Mo  \sBv0L}? 08/s`(gPC9ۻc+&]=g]e5)000oUvKA9ۛ5^yizݩrBԯ5`o1100p@9ۛ;E/\e;ǯ9ؘ=7ѕCA Z޲MU9PNL ڥnq(D[[aDlUD^-tj =CT]/!%Ç2 ~&d`@u_ Ď` UI1b(g{s`} "Ǐ|{ 澸䫎YbaWSs^׷=C:~~=۹_Ay;LKJ}ON\J1Fܪ_:Kȩ/m~`:{fE I:ńOZ{-˹T,7_8 C=>/V!N@llo:Nf?©K*҇0ȎO7e_IWk -\.uRbwD!D\6w%O;S{w= =rwهAI[nj]U}&- ?WQ~ug);2}9'˷/8[-##]:Az=^S{t92& \L!b"_<1+?f_vx}ԚouiC c.N}xє·e[@9ۇNO=vܵ.d)[Sk9=*2:wݔu!iq\78-}2Y^+|ۍVBowp!8&WiԵZ+ˊu̽6lX3-{N۔u?Y0݌wXrHg=ZzGˬ*k2L1Y׏+ 5ڔ4W斴.oZ7}LߏgY!?i~lG'˫VVÎC.tFvTO&qUflKt]i9е7ueqxx:WȗZF5b9mcvB᭧^gw|R(9Fub(g{8*R7cZ+_{vU2y_<BǔU}1hڭ_ʏ;wl:!:|3}H=abbq,at.Z*VSSdۨ !D\yinr t;rvZ{~RP$j,Έ7WHdT$FuٺX忭^R%(,MEV6 +8/)}Ė)P7;.G؁v˹4sΊO v4zY 6>KsԞ~9Ţ4M~6LBթ73-'Kyo=aaR˿ޱ=Koq3uΫ|{1gVہ6 d8.&+gl 0{V(Zv]ε{77==kîKsgw=T%8h 9,l$_eYް)9Q3v}ܲs8~,l Ӹz\' ^jÕUwi x$i:SpzܬضM? Տ^|g,zàu5!YDZ/8hؙ &~>EQiP*@U”blyo6 GՇ0JBC\HrH-ېpq:림TS M?QFMQ7y_^Q wt_m0@)/[^&A9;Դ;bg)%tOgȩw\z"oI)z@B[^C+ `.:  ,0Y`0`(gPC9 r ,0Y`0`(gPC9 ?ӓrsbe7 446qcu}}?%.;"QmQSE_eHzچ',-%[G/{{v0; # _J99C~ Q= ~֭7nDDDdddH> 4iR`` 0455\ɓ=~pN:5w\@ə_ ,󔖖FEEݺu+"""**E򕽽3'Olgg6X555t{?~„ ]>G~hrjkkL޼y3::Z:cǎwivDIIIwލ}vzz+55577\^/^hkk_6|rvDߧoIDATt_ttt};)(CDDDuu+:MAAAfffO~̙9 ֖p۷o߽{733S[0a___###e@EݻwݻQQQ:::O8100Sz_iffʕ+8b_T*(gGݻwBxzQVV!;;[nnnte62 @9 ҒOo&srrںtf``4n8{{{l#A0CVVVzzzzzzk>!궶SB]\\tttlC~aH \(ga566wwB~رVVVXleeeRrrr&;ryܸqZZZo(B_PA(gAASSSfA~ ,򂂂\E^^^~~~QQQdzh\.X9I$?HTUUUUUUYYYVVFoኊ;D^````ii9HT _UP⒒ˊ YӛLzcihh U)^uuueed;'EUUU;:‚2HC~Y9JJJ***+**JKKeeeeeerM]]]OOАflfuttl6АBQ/B.!b[VMMM---VSS㨅BvW477 bq]]H$ Ս P( 䏼x7("rFzz3Io)=5+B__߰_+ /@PF zPXWW'y!*҇D"Q]]!ޠ{b!%2555}}}"nieQFfk=F]Jjjj\.bl }}}mmm ((=!ߚsSs-D_Tnj3f̘A T=renNН1gs(w%?K>B)=97f:wGxIoW ~V5 A1#'Fw/JY(YPUSªbq6/XsQD5WMM8BH1@_P΂SVtLB!5{m:Հԧ!oW7V(5+Bwuǒf,8O3+ʉZE8?UlUsS BW_5(cFSίMcSuidU'cܶc:EQ\G׊I"⺸ϻQ_x[/;c!-_?uQaВD%#G2a[(c؏ o){cX.EQ}j+pYs#K)DT˛\(-)/u>o{9p(Xr/"ϕ!RTPuؔ;bX,$j)NU,"MFa}}M1YQ'np֧e45cڄm6ڻNϜZAYm,Wuk0ECȭm^Ar"?yd@PtkL]BUӣ i{xyQX,=cgBsTTǡ- 4mxںu$][bf)MYW^ZY =fL/|R,W#/=WR5mp5kYX\6i( o|2m݆NG (gA%=Z{6G~JX\6EFK{g.%s ]lVE)>sDmu|}6hL^{^ w?~'=on @2SAF%Xe㶳c MI;q:ye4%1fXܘբ -7Հsoha6 15-ި8fL/^S,O#;k+!fɵ::l g܎Ď%j,gf(N6U~hLÛS?.oP1f옎;rT Ly ײ0VBQ׷4h bXj6޻nZ-/GTwE]'&[ӹ-Uc\5w撗}؅󚝓v|SWzo6|rL? ֺ|죭!;!Uʚp>m߷it2_yzQؒ?֊?Yk3__\ 7^GxƂJY: (gAU=ڗPzqӼG7$h0)O,n6fSQB9Pȣ;2|e-aJI$-a6!mznsO~˩lp1o8gbE!!$L˯j-Jlm>ֆi -u J*h[qR_dHV,0Y_oQ,ܳqoVxrݻf܆;{/~t%EP{v׊W~zg~mRWw?h.m}3djRiu7o;Sb~U#o/h-9'>8P&lS= GuK~R,O#ۚմ8\]0[:zoxս%BaIW /2$+CarM7sxZp8!,>8i{יq:jj>Wښ\g>?! w4pPKZk(Mu-9(O|C[|p]SKeu۔ܷr{op9C`R߯Hn_+i7{Jfjϫ9ޓߏcNC!Cn)y5skyK,Gj5C K>YE ny\R&|qsޒe~-;$3q 51{L w]]9 qxr97﹣e#`7DXRYPOIENDB`python-aptly-0.12.10/doc/examples/000077500000000000000000000000001322737302700167145ustar00rootroot00000000000000python-aptly-0.12.10/doc/examples/ubuntu_mirror.yaml000066400000000000000000000022571322737302700225220ustar00rootroot00000000000000mirror: # Main repository ubuntu-trusty-main: component: main distributions: - trusty ubuntu-trusty-universe: component: universe distributions: - trusty ubuntu-trusty-multiverse: component: multiverse distributions: - trusty ubuntu-trusty-restricted: component: restricted distributions: - trusty # Updates ubuntu-trusty-updates-main: component: main distributions: - trusty-updates ubuntu-trusty-updates-universe: component: universe distributions: - trusty-updates ubuntu-trusty-updates-multiverse: component: multiverse distributions: - trusty-updates ubuntu-trusty-updates-restricted: component: restricted distributions: - trusty-updates # Security ubuntu-trusty-security-main: component: main distributions: - trusty-security ubuntu-trusty-security-universe: component: universe distributions: - trusty-security ubuntu-trusty-security-multiverse: component: multiverse distributions: - trusty-security ubuntu-trusty-security-restricted: component: restricted distributions: - trusty-security python-aptly-0.12.10/doc/publisher_diff_example.png000066400000000000000000000743401322737302700223140ustar00rootroot00000000000000PNG  IHDR* pHYs  tIMEUW IDATx_PZg?rNb'1 hpvօ;S3&uuvFb;SW7MgH.6ڙh4`jS D$@8 jM5\$LJyޟy80 E]@ $?@@ @ y9n'A5~`$ys@c@C;)YLUx8g|3>Ιo|j0r)Ts80q1'Ϟrf'!^;yAcqURŋzk~rc,_psՄ@w৺E?)ċ灓bLP++:F"#poJprKֈ84{ׯVJENkyU-\ѩIeٱT.q8H^^kVJ CԴˠ8H^j'jNAEW:bnG=0O@hw|`p]4Mor4k"R\YmFxyWBheVLQ0wK*{#+7z\ۦS@?yF1:]/3l kO74C %It]LU @1 xz5H4Z0m'%@elJ@f' +4*X x+0JQ04* 0Lh¤U%Pme(LQ7 ؿycկeZ#:e 5k!GCLhnnuL0 øݐA ^MCwNS@~lXB>Ρdzt_HQ0p4xMa=DWzS0>zx"}<*~$KUh]v\Ӧeq {T&T:G+}}Ni bNT&Pk/U:Z#2L3cW?~H|(GC `VeXB>_j3}vo#5lX|:`׋=z D9+8 \` i@Jd⺍JQa \lcФkC ^M(SsH#;*Or2|q>`lGY|/ۥ[A`i;oL]-w9֔d.Rz^lR> 'PIﮞW'v^w:DRRz=+wcRfÅILѢLHjPvr!V7M-[#%M~aqdu.WVwttņz9JѮ}vdHJqf1("6X%Uٰ""XSէuwo`vxUKZa{oΘN$!۷oPo؎q &27ª֎D`:ۘf.4ZeV @PЩ/0LGo6flPӦZ,h gD6 )Sh؜] nRd u℡! K)d("d7Iߐ$baXvaHZʞ(-儗iR` IX߆mS#ӤDFu_ab{Ӥ@RMADҼN SI J1Lh$[IcLwD\u:Q#L7⺍&%f(4N/Cfcp?d]I.{X\Şy9x OrN;'Ѝx3/_ζv߰ @ H~@ $?/LΙ;}=%enz @ ^?XZ9co_0$^?)採Ɂ@ MO))nC^-gW؀EkYaiVc7z`Џǻs[-~G$|5y{[d=?`:s(T>Yz1Ac\`׬e9rަoLG Ǟk]<|\c8oOqDӟ n| vHlr1;spq.,alS FWhܘ;v`.C#h*<UUsq~:FXU$D/"sqM'xaɎfZDRw'}$-$LrZ<s;o: ;ywctuak@U}"/s]9|@_W3w%+= @ H~n@ A @ m4P_EME3[ u篜)+-----2@3*+z/)|,'_t[ӭ}nk8ɢb᩶Ҳڛ篵єi 3 8/֞Q)ߦٝ]K͆RM՞єL*%- ui醺 -&0*WޕwR K/mj,˩LfAo᩶mK1T?5ȯ$쑜82NSH`wn5C<yԉtͼt}^\I1pVn˜a-8wZYaiɕ" f'`5l?(----$0PQ{s|F]ZZiζ MTxg-MYYY|Zd-3/͘>Nėa7kpAcTKKUҲT:0enR'ꕤ+-*MYi":L5KK?6,]7Pf_k1hee c`SKwSh WauO` lEm_!N2O^Och 1$U3lW%36d^`T @YEUUEq*д/V#O9IR+*Im2؁[a n᫻oOϩ[nݺ5|8\/g.ZVGki7oiPyX"ty}֭[nJ0(:e(1\'H#3_} /]姗8\kϵ XO|Sr[#݆rn}/,8v̈́8l2R=|U3)@î|Kg$[nݺe{1oߞrkl na˸]G}8nˉ~uw+* :Ux=6@`^KL$YWµ $U_n]:agY㵔`vmeb#fW{5^;,1Ám_R+%@[^gw8ȏΕLZ/NYo·ii* _knV@,Y)g 8?$ $6<{g>a&v=',u?;pTUq l14_܁0LmSU+{wmINۆa/Rl}xC  JRV $DCӟNV4kJD0R.m3~\VVſ;E-)7,-S&R,5郓g\٥'fLpn}Ϳ4v'>6~9}\Z ՔiζYٳ[4ʟeͅ כ/Q yTҟO~qWT>T' _>lP~ѕ|\ZZ~4a%  )R-,JKKg:2?'KKK+ڜa;WH]"XQZZ-yzA`aVֳBA)Y.cڿmj]%_S-7uB _K5]/2&QW +!uAoDB Ko@7@ A @ 1 #įH~wa2xt|t@ sݣlw~mz+T}{3A q{KpvY? ԼG`Wӎnc=%0m>>Lr}ӡ@߳x9?bH__&m c;& .@վ!|\>WK#y</'>Yr|w$i7o(:_N#+ѾCh? \呷i7Q~.p󛎑ymo<ɔp0_|8J@(fiB(E OZ os!q^aƢ8 >^eH^\>\(kvs"^@D 3w =ΘhIs݋n>Z5 IDATtܹ|o>3"@'3}_NZ,D'U |OwKcx]7`$iޓfU-y<m^ &Y]nYZN p9]RQvqGQqY $y0vXAiܛ==qv鋊J"].gw]H{% <~K\-.LJOck4 XXx+G1G- ~^1eUɕ-h9\Xl@ -@ $?@ >6[i@˱j)p&W*V򓹳'wcCMqً=g8.59ጋszc3\} p\Kp(sJgV]~ Ϻk~P;ڼ;uĞo\CCV@ Y_I}?~?:8Gh{hOW;c}p&Hp 匋Q9=ekq\:ikelR>L K !%9;mn3^x> 7>shm3!nmJ;ʧE8-~df-4[;9۟c8GhZ:s9ꎀOO|Vz̷yf,H?,''Gz]if;j/s8lcٻw hù4u"ЎkZq-lg'TwQK\4ጋ'n@D=+nkǀT=#XjhY/(q19c&6jd*JBP s1 c( $wb T= ød@65i`c萆͜mM?cȐ@J6@a*aRFLP}87cJWN顷c FP]bR pfȜQalF0 (L|W-2L;k?B7ں{ e(FP OSl4ͶT+ ds-+ۘfniM|ǾbRP6;FY&س`P㪺W,LsO#uC+vtIL 1"+?Y1$GpU;׾"?vǩv{a& 0ia| N@LxKd0gQY[U\(E͸d@ސaBl6h,Zmj EJCF!`4>G&e$ u>c&k[@ӝ]A6gW4%/!,?38ZPV~'zLF}:Bׄǧ(=Gm[mL34K&ǦQ(ieO2N$G}u#1MSE}v(,sكeb#?o 0[n>ɀi ?Tm".@?W&}>ƿsp xR!Ľ~iA7)~d]?y@9@ ryrbﰊ9~8me[nÆ#NXvf,ļc >~|vc̳4{8dž?anw1\h]12O$'>-hE^[fp}U@dxw]ɽBb :a@r{?nuԏާ]^""tЁgWꖣCb8c!Gl2N]ՅʚٖYE^q bs@4^q\7M`hbo~dy[E+"s[_0es|4zOȂw#cKPkxgd"L,f50~fp[M$hυrIng˓]":J[.wHw+qy + WyePqyɱR<2cC:K[9:yJu.L~kS'}Gb,xܙ[J?WRg5s65Cnz7iBt2>fˉ'U?%6喎Ure<ˉնR+5j[g˗V60 aZseB/ ~J2alv6&i(PfW|oG@&}gyFK~ (\ȑX:8n\]u޲gwͿ-n82]h-lHHxqkاeKL=00XUYFU)wBX'#7,!?쥁4ըϬnneW~9 [%#͑94Y ܽɉ<ҥ珆gtVs :]h QAߘN[:2¦#<6|K^@[x{?:eG\C$q4/ m^tr|O tbp|x5 ~Zhјc40֎.o>]6[b ( H~W Q)s՛ r¡r"AC /z5@ @ H~@ $?@@ @ A @ @ H~@ $?@@ @ A @ =1?;Vޱ1/ 7]~⁦{s+);X-єag8}>? 9wt1oD~=<S4frs~]՟\uR"-8|%sf!*!&TU񔻺OVT-9ݥp8fN[M* -~kkTऺϽ8u[AI!UVw7=V#@hwyz_9!lWoMd4{_[lg ^mRpjJYy%`~DTK T=x:e𽊪bn$YF7^ Eh]/71{.LѪ ԏdBJ020gI\P)b6F Huz@4Nh"2@2x^ f?{CCdXJ5}*〇ԍ)bF]v&Qi5 aJC#z!&QiT 0M M4uzlНPq9X>Ρ G7m( 'f{ԾIw띳`O =Q ydSsXR?)7M)Jpwc$ 5$ (zh1 Ä]Bo=<*~}HCk TNJ,LV7J$gS:e nY2!BJgrf5 Us i 4`BJߙ\BFL eZӈo=I' 0L8 %g/; j(:FrFt&iB"&sl5n`߀N ii7ph(tpefoH'׍/e>jغ= M=&j7ڮ&3ڷY!}g/0!5ѣ ?m>>T'NM}b}2>;{91D;C _BФUlcQڣiW8USc\|ݪU&G%'u7HVW:n@b^^>;bU[ W&4< "g oطqojJ}+0ݍsAl_O=7oyrVEm}cWH]m,gAU8rz&r3 !-p#,$ -dqyA/$cJub8ApwU~$& 2h5"oW=IT:%mBym5oQ~xy0J#Y;3ll)5zhWy ^b^- $d~wCD Ľ4=vU_s_y5%ak1t CW" YVMVz+[BtXt%z9Xƞ&H9óBMe^໳:E)¤@7ԢA+5f'$Sbe7oerY{,f4ۤF rpI$A*i? p>kL}}MJ)8IH~'R٬j5;qZR Dj9h2:׷2>8Ԥnnq4e0 }`ͭQ$pD@Q+V_# bZ?p!mZQy@PzVCE-zr4{}7+Sw9fuRrg,h{r=#yKOfRmZsug~CgMk7٣ DF}Ki}+[&|ta;{Kx D~FQ8)(VP(e&=c{0cBN3gV2y?GFQ09;$PSu@M6Ķ[?CZt+/l'͊d<Fz2U/{! 0I'\fp=0>PNXS$5Iٳ`#.Ml/gG2EGm&扯 %7|I/r5Y- !W(?$vD֜7D|t*6Q? }tGha7M욱iu'4aRa=ՓMBij{V ]97xE'B+N;\z$Pfa&QQP2=ݽ)g(J6B Bb)a1{mXŦOK Tu,44w_5Ş|)͍DRNݝ$N=JlCNL0 5JۨSH*}oSCi6OP=uZ8yW֭-N!ډ%DeI֩سFb{aPB hMCo`q ԩl^h@q#) 0QV"*+2^%+Uu 37i&T:S-@ VK&GL~sk^D8_{7,?8Ph9{3:EtQB1y,7E4{ 0!D0 @ $?@@ &}>G@ aY tܿ#LV~O7=C@ ܟ[|-C/Uys@ WǦt'j\ס^?URl5,p4SEwݱ?v0]rG] e#\xxuqἽ-b jB*,t=1n.jkݲCko \jgcOJߵ.Y>w{1鈸OvJ9\3Q"9E @ S8>[34'%ιP\Ўt(0Lg] FB.H<::۽e:\SPgZpcgܻe-m`^?,Ϯ,jm6Š/s\`rem1H̙A2C BQ`۷+k+y5qGh h:zHC\%G.TF4sI'ɭ]#<~?H%%P>đ -^5s{gDnu2hb'lKMc'2] }?d*@l vcwr#\.YÖ٦@ܝ182KpcxKہs\p:::0<+{X->ːh5UJ% )hZ-WOfɜK9@c=8'N39t`,@ ;ρ IDATg ~O Aؽܢ8 ={Yq JG]|aeZp5-q\rw4@Tyn\rn0?o\͋ ~^.xy~_M)x!Gt?+,pr{ !|)0=O`{ ڟD8}p\pd3ɕkT g+JK|yv6q3f-ڛ3RM5vUhFsj,KZk-MYYY|ZrǬ]u~vxݚ٠1=3겲2ҿnq'pq87& ZpS͚KuOmL,[ ek!ꄝm'?VOO;?T8avk@YEUUE1?<^[QQ|z{'S{zT>`E+ٛTWUU*JH`)/23Nj0ԧ?όzk$#O}DF<墪K _Ra_՜|u[換 uKצmj0w1o%d5hͦ~xq: @љ+FWRU+yMU˙&O_8Um̕*O%e:۲tZ7lWZ ӊ,*/t39k||pz}:Sx<8hs/2"᩶[k7O?p׼.'@ 7-nx8 eY WU&,34\*aH mz׿jAo1{mOjS˳ԥ+a!xY`iM6O;r]e !@n9õ6MKFWDׯ,>.):^QK jԹSZ rTr߆^{=@f ƼhJT!1 7HX1SeYгUWY*5 텍^+Uг0(Up ׍$X0X%]H H?G?yNG\ z+Bm Uwfɶj}9 t볋ii(wB[mN3u^ )LI q0H$P4 ޓ}ͤJrgfTsN"<34-A3u.UY!ԟxݏT{j)͡$68j>\iR֜ߔS-se_59u$jиq}za|DjoF/uI kF!ąt9]P`F}߅##<;?=u^ ^|jz5T&> Cm :7Js]EH&N\9rcλBz0'#/LOC39?=?Zl$] IVM v# -N>TmsvWvhlg`~4u(JR5o kn3csc{ ;릜~hlF(KxzO>zTuM{Z$תp'>=>tkaեZf"os~0O73":{J{xkD}/D'{O~=~HDAՁO>||j'$Lho\Z7H[]m|:pn :3;rKH^#3uO%C=sI#h@ݩgGnR7L}g =`aϬ۪݀֓ߌK 3=mgd2#6o@7@ @ ~@'˧F@ וm|j?%S#w臶@ PH3HAd^.Y?/@2(S^TO}H'?_wZ/2J?v>y~ެ5~$b/;%dDݒ װAVV~@L;Գo.%\ =>W(Gl=?ё[r!sqԬ <^93< PH)\5;o/wG޽A R]<<^0ءQ|x&~˿Cgl2"ngc_,=侗'/+g|D>JgS%@/]z\]/y #?2/u??='~X%| bvo_+XWu[}ˏ튤 :;uJA R {׼33s'\Vi|۽`MŖm@x`WmUE#_ Q7 A޿?˗ 9s?GF'.w>ɝL#_ ̳xy_?^<՘1`?܋Ml G>@< ;wkޭ#/(? K4z^@ %u??o|o|->1 վj׏>|kĞ^xɋ/*4%rb;=ۋH⓫O|ȽGB`Slnn^|K7C>ċ9^n?@r8PG ݔ<ˌ`Ko>H{w*ݚ7b? 6tև"} w^?C_ }g. 2q5w ͤW-0 Qd6*0wԢ7۴EjȦ)w5V3&#>*H|< )Y}Po~x-w-x34y/m~^8%hy[=AAYC9n1uoyQ>B\pbkެv5zd۔]B.UmpPndj[5wG56X'z}$uݧ{IUVoBw=37R4ѻd/Y+V׷&}_k$d #}T sSϩZ)HzU|0RS0Q:16iuM2סMVv*g&i:*!* .1OJqF|`{~KK1V :tx<.]tsC6ݤM)P.S?6w>jO1#VIbKaj(=BM괒V"I.y.$JdX8ơqQbXU[: W5+PʞG!ą̇oq<&Kp) u*06ҩA iEgƂ~ Ǖ>WWa+1q{86gY+apWKK:l*f;ok 'xSN~!-Ī/"HIqR[us~nd*`-a%<[@؊ɤ "dKwJr/fL[cC4(,.:si`9 oEgi<z\Ȧ%t^g_8#@g_Kȸ[3\8ZbKSS\“%=EVD8fҪFT=]/NJ[6w3,oo]9ܹijJ3(SgBQ.΍ Er  ܰNǤ]=n_bw^kR8Ԣkb3E(+v"{W9kOw'k._?y(ocɎ}mX@Rcb9<2 ^vXUH/2 Bl880~&p&hab`RP(*CeX4dčx|yA U^3bTTIƣ]ƦRԑ:xaU%𙉉 TTe+>`Z R Pl8x^$ *@\{EFeeӉr[8blZNFPqLtaؠ0ֿ!՞~L)) Ӿ+RɄL84!H ۺq84:OAgryw,ՐMiSq[tT4C[W4y<ѩ&zl,7'Nli=ڬgϓ~tl-־㔾]gɯN;j٫}tlM.Kӝauۉw?%d7gf\VVo8y}u/2MfHe2РJvCYfPۺNHSaJd#Ml|Cf$Eu n1t"S]*k'JEћ 1MkEՉ{m3ENo>7 iYm*eWǗoTFނ;vryebr:hMm 7'o53z?<- ѹ;%T+<>xu@h0NON2ׇ:$ɦ}Mn/@վi53=} ,11=L]ߕ۷϶- Y-BNm6p|=ʬ&y}GXZY6xct,5ɳ7IHҔryt7wAn"y(YDΖ!X<[Th%`ɷUg/=VkzG(+!JS@)]nZ33:;:[emIYM c+3㧴oUƞSW/tne2I4"%յ')%Ɉ} jdXzrsQ&b tG[&zBvpYCp~Ӿ<u0ă.:ŠB[V4p)w1 +8]iaS:iA rbAŠlqY^^_TN@8bZ|M[mdӭ۔)҉i(Td},aV !+^ϻ}+Y%_=n;vyzTۆrs cz{"cGK=wӥ>'y,TE?ݤ!;Ԉl#teY!@ |C r?@sm Fj|Ux^Rgpty0Q:3YaߢIu/ ͦДS[&,H15enC'l(7jOONNd)n2C6I- rSwɭv3u}WoO\_ =IgNpz/Es}زnܒUI-=;iULR;ҲgF{=h o"`ighNc#-Mߛw" îTj`{eaxB950҇(hP Y\.u\ o"4.dT Mf@ \x[UGɱ2 .xsWcQiU Q#L{D4B_lfXZ182 s̹sSg?doo(@tqnIU0;h o:uv}IzI%EsJF 0Y>@lSd 'uEAbRZ'zuP) "|v0ͪWހuZ\ Ԗs}Ȧ[tL豗jE@kJwHj ֳ]-ܜpbP^ZSrx<.hir[[[N~SpwB=ٚK~[?h o;G^x-µêJU2$L,jI0Y5d' pڝc*SvTJeSG#+̧t*N R Pl8x^$ *@\{ErNoQo|ٴ69\s>°Ayp36F 7@ g!:?c'\b])!幵 A#߳)!eXWF @%@ A r?$@aYoyu9xCdD,׸9" ^kAYG!=rs4Y+CWJﳓ 2EYzqeB5xEupht %$*io|Y 6!!Ӧ9V)Zi^`x-[~eh6+F%R E'gmBLmJ6OXhDd}> 8Kkʜ)t)!ͦp2:Ë f .kЙ᱿c N<?ݭ׮tpӝ8ŕ7Ovzx<?7j'ouڢ[]fsowt^~Е;zs凗ֳg;[Zc8+pDd&= XZeAՕlL|Ͱ$x#Kdx.%#Mv&J7 ƛd#'J*VDP)Ril^㑩.aεFU%ͅD5ĽHAQ䙀"7WU É ٴ6M +7* # =Ny@rs[E{xSn,7ljb gk&$.C~f8>qٞ`e,>#, ,<ϳ/픗p;\Ax䦡|r`]9M=?>46oxFgm*pk2~*YǢҞ7"%=R_-6wv'&7}oSʪ zy&,Mk>vv{dPt5\c ljb $009fd”m# 94BpeAbGr'ԕV̐r#L4Ģn@dT 8Ak=Fq(VZ4 շKj2tbM( zMi %0.J'b޽'],L@Ɲ|ﶛH8J6ey,flxl69b ~IVb=rskXbUDU.]i2tBIW5 I!gh5ES5lZNΟoZ#F + D ͽIap$C@ 7Ҡ?Al@2tDY<*HN{.4rG3@t)92o@7@ @ ~%CDUZV!rsߦ39}Bn௳M9ڢ3O9xBP?h+lJ2H@ z,\.q)zQQ[i.Z=zh*!ӦBCj[(s]fw9 ~햛HmNR"8c˨,BH501谸0gx&=o/LMZLm(•@xE8d2T z>cG*]Tnr)Hn89[ZO'9ĨPj`W"SEU%k3qPp&Y$X6 TB5p/{0051pAP125c*}ͩ:%UC6-MrMkd"S]ʴ+E rG=\<"07s~w@c3ũr.s>fTExAo"m*rA~ _ [^^cMlĖv r],g~4;x2}3,-"0vrbY ) >s sYE8dTd6eWgN`*7lYri \ )X2՘]%ޔzұ 0a˦'7r YGts6}630uS%?()oT5kLc|1J0v[ lv$E@6\$G7*P ,)Q(j1,S2| (|*"i l(.:̣(y)\} f > Ťr5skgPob]R@=L` ǸmBA"rBAsNdtpȦ[)﷑6-QH r?%Hn@9[;'PdTK߲ v% Ù{m=0.14', 'PyYaml', 'python-apt', ], entry_points={ 'console_scripts': ['aptly-publisher = aptly.publisher.__main__:main'] }, classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Console', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', ], keywords='aptly debian repository', )