def test_get_newest_in_either_map_old_first(self): '''Tests that get_newest_in_either_map works if hostclass not in first list''' list_a = [self.mock_ami("mhcfoo 1"), self.mock_ami("mhcbar 2")] list_b = [self.mock_ami("mhcfoo 3"), self.mock_ami("mhcbar 1"), self.mock_ami("mhcmoo 2")] list_c = [list_b[0], list_a[1], list_b[2]] map_a = {DiscoBake.ami_hostclass(ami): ami for ami in list_a} map_b = {DiscoBake.ami_hostclass(ami): ami for ami in list_b} map_c = {DiscoBake.ami_hostclass(ami): ami for ami in list_c} self.assertEqual(self._ci_deploy.get_newest_in_either_map(map_a, map_b), map_c)
def test_get_newest_in_either_map_old_first(self): '''Tests that get_newest_in_either_map works if hostclass not in first list''' list_a = [self.mock_ami("mhcfoo 1"), self.mock_ami("mhcbar 2")] list_b = [ self.mock_ami("mhcfoo 3"), self.mock_ami("mhcbar 1"), self.mock_ami("mhcmoo 2") ] list_c = [list_b[0], list_a[1], list_b[2]] map_a = {DiscoBake.ami_hostclass(ami): ami for ami in list_a} map_b = {DiscoBake.ami_hostclass(ami): ami for ami in list_b} map_c = {DiscoBake.ami_hostclass(ami): ami for ami in list_c} self.assertEqual( self._ci_deploy.get_newest_in_either_map(map_a, map_b), map_c)
def setUp(self): self._bake = DiscoBake(config=MagicMock(), connection=MagicMock()) self._bake.promote_ami = MagicMock() self._bake.ami_stages = MagicMock( return_value=['untested', 'failed', 'tested']) self._bake.get_ami_creation_time = DiscoBake.extract_ami_creation_time_from_ami_name self._amis = [] self._amis_by_name = {} self.add_ami('mhcfoo 0000000001', 'untested', 'astro', 'unavailable') self.add_ami('mhcbar 0000000002', 'tested') self.add_ami('mhcfoo 0000000004', 'tested', 'astro') self.add_ami('mhcfoo 0000000005', 'failed') self.add_ami('mhcbar 0000000001', 'tested', 'someone_else', 'unavailable') self._bake.get_amis = MagicMock(return_value=self._amis)
def test_extra_tags_no_override(self, mock_tag_ami): '''Test that additional tags do not override asiaq tags''' ami = self._amis_by_name["mhcbar 0000000001"] self._bake._tag_ami_with_metadata(ami=ami, hostclass="mhcbar", source_ami_id='mock_source', stage='mock_stage', productline='mock_productline', extra_tags={ 'mock': 'gecko', 'baker': 'innocent_user' }) mock_tag_ami.assert_called_once_with( ami, { "source_ami": "mock_source", "hostclass": "mhcbar", "stage": "mock_stage", "productline": "mock_productline", "is_private": "False", "baker": "mock_user", "version-asiaq": DiscoBake._git_ref(), "mock": "gecko" })
def setUp(self): self._bake = DiscoBake(config=MagicMock(), connection=MagicMock()) self._bake.promote_ami = MagicMock() self._bake.ami_stages = MagicMock(return_value=['untested', 'failed', 'tested']) self._bake.get_ami_creation_time = DiscoBake.extract_ami_creation_time_from_ami_name self._amis = [] self._amis_by_name = {} self.add_ami('mhcfoo 1', 'untested', 'astro') self.add_ami('mhcbar 2', 'tested') self.add_ami('mhcfoo 4', 'tested', 'astro') self.add_ami('mhcfoo 5', 'failed') self.add_ami('mhcbar 1', 'tested', 'someone_else') self._bake.get_amis = MagicMock(return_value=self._amis)
def test_is_private_tag(self, mock_tag_ami): '''Test that additional tags are applied to AMI if specified''' ami = self._amis_by_name["mhcbar 0000000001"] self._bake._tag_ami_with_metadata(ami=ami, hostclass="mhcbar", source_ami_id='mock_source', stage='mock_stage', productline='mock_productline', is_private=True, extra_tags={'mock': 'gecko'}) mock_tag_ami.assert_called_once_with( ami, { "source_ami": "mock_source", "hostclass": "mhcbar", "stage": "mock_stage", "productline": "mock_productline", "is_private": "True", "baker": "mock_user", "version-asiaq": DiscoBake._git_ref(), "mock": "gecko" })
def run(): """Parses command line and dispatches the commands""" config = read_config() args = docopt(__doc__) configure_logging(args["--debug"]) env = args["--environment"] or config.get("disco_aws", "default_environment") force_deployable = None if args["--deployable"] is None else is_truthy( args["--deployable"]) pipeline_definition = [] if args["--pipeline"]: with open(args["--pipeline"], "r") as f: reader = csv.DictReader(f) pipeline_definition = [line for line in reader] aws = DiscoAWS(config, env) if config.has_option('test', 'env'): test_env = config.get('test', 'env') test_aws = DiscoAWS(config, test_env) else: test_aws = aws bake = DiscoBake(config, aws.connection) if args["--ami"] and args["--hostclass"]: image = bake.get_image(args["--ami"]) if args["--hostclass"] != bake.ami_hostclass(image): logger.error('AMI %s does not belong to hostclass %s', args["--ami"], args["--hostclass"]) sys.exit(1) vpc = DiscoVPC.fetch_environment(environment_name=env) deploy = DiscoDeploy(aws, test_aws, bake, DiscoGroup(env), DiscoELB(vpc), DiscoSSM(environment_name=env), pipeline_definition=pipeline_definition, ami=args.get("--ami"), hostclass=args.get("--hostclass"), allow_any_hostclass=args["--allow-any-hostclass"]) if args["test"]: try: deploy.test(dry_run=args["--dry-run"], deployment_strategy=args["--strategy"], ticket_id=args["--ticket"], force_deployable=force_deployable) except RuntimeError as err: logger.error(str(err)) sys.exit(1) elif args["update"]: try: deploy.update(dry_run=args["--dry-run"], deployment_strategy=args["--strategy"], ticket_id=args["--ticket"], force_deployable=force_deployable) except RuntimeError as err: logger.error(str(err)) sys.exit(1) elif args["list"]: missing = "-" if pipeline_definition else "" if args["--tested"]: for (_hostclass, ami) in deploy.get_latest_tested_amis().iteritems(): print("{} {:40} {}".format( ami.id, ami.name.split()[0], deploy.get_integration_test(ami.name.split()[0]) or missing)) elif args["--untested"]: for (_hostclass, ami) in deploy.get_latest_untested_amis().iteritems(): print("{} {:40} {}".format( ami.id, ami.name.split()[0], deploy.get_integration_test(ami.name.split()[0]) or missing)) elif args["--failed"]: for (_hostclass, ami) in deploy.get_latest_failed_amis().iteritems(): print("{} {:40} {}".format( ami.id, ami.name.split()[0], deploy.get_integration_test(ami.name.split()[0]) or missing)) elif args["--testable"]: for ami in deploy.get_test_amis(): print("{} {:40} {}".format( ami.id, ami.name.split()[0], deploy.get_integration_test(ami.name.split()[0]) or missing)) elif args["--updatable"]: for ami in deploy.get_update_amis(): print("{} {:40} {}".format( ami.id, ami.name.split()[0], deploy.get_integration_test(ami.name.split()[0]) or missing)) elif args["--failures"]: failures = deploy.get_failed_amis() for ami in failures: print("{} {:40} {}".format( ami.id, ami.name.split()[0], deploy.get_integration_test(ami.name.split()[0]) or missing)) sys.exit(1 if failures else 0)
def run(): """Parses command line and dispatches the commands""" config = read_config() parser = get_parser() args = parser.parse_args() configure_logging(args.debug) environment_name = args.env or config.get("disco_aws", "default_environment") aws = DiscoAWS(config, environment_name=environment_name) if args.mode == "provision": hostclass_dicts = [{ "sequence": 1, "hostclass": args.hostclass, "instance_type": args.instance_type, "extra_space": args.extra_space, "extra_disk": args.extra_disk, "iops": args.iops, "smoke_test": "no" if args.no_smoke else "yes", "ami": args.ami, "min_size": args.min_size, "desired_size": args.desired_size, "max_size": args.max_size, "chaos": "no" if args.no_chaos else None }] aws.spinup(hostclass_dicts, testing=args.testing) elif args.mode == "listhosts": instances = aws.instances_from_hostclass( args.hostclass) if args.hostclass else aws.instances() instances_filtered = [i for i in instances if i.state != u"terminated"] instances_sorted = sorted(instances_filtered, key=lambda i: (i.state, i.tags.get("hostclass", "-"), i.tags.get("hostname", "-"))) instance_to_private_ip = { i.id: get_preferred_private_ip(i) for i in instances_sorted } most = args.all or args.most if args.ami_age or args.uptime or most: bake = DiscoBake(config, aws.connection) ami_dict = bake.list_amis_by_instance(instances) now = datetime.utcnow() for instance in instances_sorted: line = u"{0} {1:<30} {2:<15}".format( instance.id, instance.tags.get("hostclass", "-"), instance.ip_address or instance_to_private_ip[instance.id]) if args.state or most: line += u" {0:<10}".format(instance.state) if args.hostname or most: line += u" {0:<1}".format( "-" if instance.tags.get("hostname") is None else "y") if args.owner or most: line += u" {0:<11}".format(instance.tags.get("owner", u"-")) if args.instance_type or most: line += u" {0:<10}".format(instance.instance_type) if args.ami or most: line += u" {0:<12}".format(instance.image_id) if args.smoke or most: line += u" {0:<1}".format( "-" if instance.tags.get("smoketest") is None else "y") if args.ami_age or most: creation_time = bake.get_ami_creation_time( ami_dict.get(instance.id)) line += u" {0:<4}".format( DiscoBake.time_diff_in_hours(now, creation_time)) if args.uptime or most: launch_time = dateutil_parser.parse(instance.launch_time) now_with_tz = now.replace( tzinfo=launch_time.tzinfo) # use a timezone-aware `now` line += u" {0:<3}".format( DiscoBake.time_diff_in_hours(now_with_tz, launch_time)) if args.private_ip or args.all: line += u" {0:<16}".format(instance_to_private_ip[instance.id]) if args.availability_zone or args.all: line += u" {0:<12}".format(instance.placement) if args.productline or args.all: productline = instance.tags.get("productline", u"unknown") line += u" {0:<15}".format( productline if productline != u"unknown" else u"-") print(line) elif args.mode == "terminate": instances = instances_from_args(aws, args) terminated_instances = aws.terminate(instances) print("Terminated: {0}".format(",".join( [str(inst) for inst in terminated_instances]))) elif args.mode == "stop": instances = instances_from_args(aws, args) stopped_instances = aws.stop(instances) print("Stopped: {0}".format(",".join( [str(inst) for inst in stopped_instances]))) elif args.mode == "exec": instances = instances_from_args(aws, args) exit_code = 0 for instance in instances: _code, _stdout = aws.remotecmd(instance, [args.command], user=args.user, nothrow=True) sys.stdout.write(_stdout) exit_code = _code if _code else exit_code sys.exit(exit_code) elif args.mode == "isready": instances = instances_from_args(aws, args) if not instances: print("No instances found") ready_count = 0 for instance in instances: name = "{0} {1}".format(instance.tags.get("hostname"), instance.id) print("Checking {0}...".format(name)) try: aws.smoketest_once(instance) print("...{0} is ready".format(name)) ready_count += 1 except SmokeTestError: print("..{0} failed smoke test".format(name)) except TimeoutError: print("...{0} is NOT ready".format(name)) sys.exit(0 if ready_count == len(instances) else 1) elif args.mode == "tag": for instance in aws.instances(instance_ids=args.instances): instance.remove_tag(args.key) if args.value: instance.add_tag(args.key, args.value) elif args.mode == "spinup": with open(args.pipeline_definition_file, "r") as f: reader = csv.DictReader(f) hostclass_dicts = [line for line in reader] aws.spinup(hostclass_dicts, stage=args.stage, no_smoke=args.no_smoke, testing=args.testing) elif args.mode == "spindown": with open(args.pipeline_definition_file, "r") as f: reader = csv.DictReader(f) hostclasses = [line["hostclass"] for line in reader] aws.spindown(hostclasses) elif args.mode == "spindownandup": with open(args.pipeline_definition_file, "r") as f: reader = csv.DictReader(f) hostclass_dicts = [line for line in reader] hostclasses = [d["hostclass"] for d in hostclass_dicts] aws.spindown(hostclasses) aws.spinup(hostclass_dicts) elif args.mode == "gethostclassoption": try: print(aws.hostclass_option(args.hostclass, args.option)) except NoOptionError: print("Hostclass %s doesn't have option %s." % (args.hostclass, args.option)) elif args.mode == "promoterunning": aws.promote_running_instances_to_prod(args.hours * 60 * 60)
def run(): """Parses command line and dispatches the commands""" parser = get_parser() args = parser.parse_args() configure_logging(args.debug) if args.mode == "bake": extra_tags = OrderedDict(tag.split(':', 1) for tag in args.tags) bakery = DiscoBake(use_local_ip=args.use_local_ip) bakery.bake_ami(args.hostclass, args.no_destroy, args.source_ami, args.stage, args.is_private, extra_tags=extra_tags) elif args.mode == "create": HostclassTemplating.create_hostclass(args.hostclass) elif args.mode == "promote": bakery = DiscoBake() ami = bakery.get_image(args.ami) bakery.promote_ami(ami, args.stage) if args.promote_to_prod: bakery.promote_ami_to_production(ami) elif args.mode == "hostclasspromote": bakery = DiscoBake() bakery.promote_latest_ami_to_production(args.hostclass) elif args.mode == "listrepo": bakery = DiscoBake() repo = bakery.repo_instance() if repo: print(repo.ip_address) elif args.mode == "listamis": ami_ids = [args.ami] if args.ami else None instance_ids = [args.instance] if args.instance else None bakery = DiscoBake() amis = sorted(bakery.list_amis(ami_ids, instance_ids, args.stage, args.product_line, args.state, args.hostclass), key=bakery.ami_timestamp) headers, output = bakery.tabilize_amis(amis=amis, in_prod=args.in_prod, show_tags=args.show_tags) print_table(headers=headers, rows=output) if not amis: sys.exit(1) elif args.mode == "liststragglers": bakery = DiscoBake() for hostclass, image in bakery.list_stragglers(args.days).iteritems(): print("{0}\t{1}".format(hostclass, image.id if image else '-')) elif args.mode == "listlatestami": bakery = DiscoBake() ami = bakery.find_ami(args.stage, args.hostclass) if ami: headers, output = bakery.tabilize_amis(amis=[ami], in_prod=args.in_prod, show_tags=args.show_tags) print_table(headers=headers, rows=output) else: sys.exit(1) elif args.mode == "deleteami": bakery = DiscoBake() bakery.delete_ami(args.ami) elif args.mode == "cleanupamis": bakery = DiscoBake() exclude_amis = args.exclude_amis.split(',') bakery.cleanup_amis(args.hostclass, args.product_line, args.stage, args.days, args.count, args.dryrun, exclude_amis)
def run(): """Parses command line and dispatches the commands""" config = read_config() parser = get_parser() args = parser.parse_args() configure_logging(args.debug) environment_name = args.env or config.get("disco_aws", "default_environment") aws = DiscoAWS(config, environment_name=environment_name) if args.mode == "provision": hostclass_dicts = [{ "sequence": 1, "hostclass": args.hostclass, "instance_type": args.instance_type, "extra_space": args.extra_space, "extra_disk": args.extra_disk, "iops": args.iops, "smoke_test": "no" if args.no_smoke else "yes", "ami": args.ami, "min_size": args.min_size, "desired_size": args.desired_size, "max_size": args.max_size, "chaos": "no" if args.no_chaos else None, "spotinst": args.spotinst, "spotinst_reserve": args.spotinst_reserve }] aws.spinup(hostclass_dicts, testing=args.testing) elif args.mode == "listhosts": instances = aws.instances_from_hostclass(args.hostclass) if args.hostclass else aws.instances() instances_filtered = [i for i in instances if i.state != u"terminated"] instances_sorted = sorted(instances_filtered, key=lambda i: (i.state, i.tags.get("hostclass", "-"), i.tags.get("hostname", "-"))) instance_to_private_ip = {i.id: get_preferred_private_ip(i) for i in instances_sorted} most = args.all or args.most if args.ami_age or args.uptime or most: bake = DiscoBake(config, aws.connection) ami_dict = bake.list_amis_by_instance(instances) now = datetime.utcnow() for instance in instances_sorted: line = u"{0} {1:<30} {2:<15}".format( instance.id, instance.tags.get("hostclass", "-"), instance.ip_address or instance_to_private_ip[instance.id]) if args.state or most: line += u" {0:<10}".format(instance.state) if args.hostname or most: line += u" {0:<1}".format("-" if instance.tags.get("hostname") is None else "y") if args.owner or most: line += u" {0:<11}".format(instance.tags.get("owner", u"-")) if args.instance_type or most: line += u" {0:<10}".format(instance.instance_type) if args.ami or most: line += u" {0:<12}".format(instance.image_id) if args.smoke or most: line += u" {0:<1}".format("-" if instance.tags.get("smoketest") is None else "y") if args.ami_age or most: creation_time = bake.get_ami_creation_time(ami_dict.get(instance.id)) line += u" {0:<4}".format(DiscoBake.time_diff_in_hours(now, creation_time)) if args.uptime or most: launch_time = dateutil_parser.parse(instance.launch_time) now_with_tz = now.replace(tzinfo=launch_time.tzinfo) # use a timezone-aware `now` line += u" {0:<3}".format(DiscoBake.time_diff_in_hours(now_with_tz, launch_time)) if args.private_ip or args.all: line += u" {0:<16}".format(instance_to_private_ip[instance.id]) if args.availability_zone or args.all: line += u" {0:<12}".format(instance.placement) if args.productline or args.all: productline = instance.tags.get("productline", u"unknown") line += u" {0:<15}".format(productline if productline != u"unknown" else u"-") if args.securitygroup or args.all: line += u" {0:15}".format(instance.groups[0].name) print(line) elif args.mode == "terminate": instances = instances_from_args(aws, args) terminated_instances = aws.terminate(instances) print("Terminated: {0}".format(",".join([str(inst) for inst in terminated_instances]))) elif args.mode == "stop": instances = instances_from_args(aws, args) stopped_instances = aws.stop(instances) print("Stopped: {0}".format(",".join([str(inst) for inst in stopped_instances]))) elif args.mode == "exec": instances = instances_from_args(aws, args) exit_code = 0 for instance in instances: _code, _stdout = aws.remotecmd(instance, [args.command], user=args.user, nothrow=True) sys.stdout.write(_stdout) exit_code = _code if _code else exit_code sys.exit(exit_code) elif args.mode == "exec-ssm": ssm = DiscoSSM(environment_name) if args.parameters: parsed_parameters = parse_ssm_parameters(args.parameters) else: parsed_parameters = None instances = [instance.id for instance in instances_from_args(aws, args)] if ssm.execute(instances, args.document, parameters=parsed_parameters, comment=args.comment): sys.exit(0) else: sys.exit(1) elif args.mode == "isready": instances = instances_from_args(aws, args) if not instances: print("No instances found") ready_count = 0 for instance in instances: name = "{0} {1}".format(instance.tags.get("hostname"), instance.id) print("Checking {0}...".format(name)) try: aws.smoketest_once(instance) print("...{0} is ready".format(name)) ready_count += 1 except SmokeTestError: print("..{0} failed smoke test".format(name)) except TimeoutError: print("...{0} is NOT ready".format(name)) sys.exit(0 if ready_count == len(instances) else 1) elif args.mode == "tag": for instance in aws.instances(instance_ids=args.instances): instance.remove_tag(args.key) if args.value: instance.add_tag(args.key, args.value) elif args.mode == "spinup": hostclass_dicts = read_pipeline_file(args.pipeline_definition_file) aws.spinup(hostclass_dicts, stage=args.stage, no_smoke=args.no_smoke, testing=args.testing) elif args.mode == "spindown": hostclasses = [line["hostclass"] for line in read_pipeline_file(args.pipeline_definition_file)] aws.spindown(hostclasses) elif args.mode == "spindownandup": hostclass_dicts = read_pipeline_file(args.pipeline_definition_file) hostclasses = [d["hostclass"] for d in hostclass_dicts] aws.spindown(hostclasses) aws.spinup(hostclass_dicts) elif args.mode == "gethostclassoption": try: print(aws.hostclass_option(args.hostclass, args.option)) except NoOptionError: print("Hostclass %s doesn't have option %s." % (args.hostclass, args.option)) elif args.mode == "promoterunning": aws.promote_running_instances_to_prod(args.hours * 60 * 60)
class DiscoBakeTests(TestCase): '''Test DiscoBake class''' def mock_ami(self, name, stage=None, product_line=None, state=u'available', is_private=False, block_device_mapping=None): '''Create a mock AMI''' def _mock_get(tag_name, default=None): if tag_name == "productline": return product_line if product_line else default if tag_name == "stage": return stage if stage else default if tag_name == "is_private": return 'True' if is_private else 'False' ami = create_autospec(boto.ec2.image.Image) ami.name = name ami.tags = MagicMock(get=_mock_get) ami.id = 'ami-' + ''.join( random.choice("0123456789abcdef") for _ in range(8)) ami.state = state ami.block_device_mapping = block_device_mapping or {} return ami def add_ami(self, name, stage, product_line=None, state=u'available'): '''Add one Instance AMI Mock to an AMI list''' ami = self.mock_ami(name, stage, product_line, state) assert ami.name == name assert ami.tags.get("stage") == stage assert ami.tags.get("productline") == product_line self._amis.append(ami) self._amis_by_name[ami.name] = ami return ami def setUp(self): self._bake = DiscoBake(config=MagicMock(), connection=MagicMock()) self._bake.promote_ami = MagicMock() self._bake.ami_stages = MagicMock( return_value=['untested', 'failed', 'tested']) self._bake.get_ami_creation_time = DiscoBake.extract_ami_creation_time_from_ami_name self._amis = [] self._amis_by_name = {} self.add_ami('mhcfoo 0000000001', 'untested', 'astro', 'unavailable') self.add_ami('mhcbar 0000000002', 'tested') self.add_ami('mhcfoo 0000000004', 'tested', 'astro') self.add_ami('mhcfoo 0000000005', 'failed') self.add_ami('mhcbar 0000000001', 'tested', 'someone_else', 'unavailable') self._bake.get_amis = MagicMock(return_value=self._amis) def test_get_phase1_ami_id_success(self): '''Test that get_phase1_ami_id uses find_ami properly on success''' ami = Mock() type(ami).id = PropertyMock(return_value='ami-abcd1234') self._bake.ami_stages = Mock(return_value=['a', 'b', 'c']) self._bake.find_ami = Mock(return_value=ami) self._bake.hc_option = Mock(return_value="mhcphase1") self.assertEqual("ami-abcd1234", self._bake._get_phase1_ami_id(hostclass="mhcntp")) self._bake.find_ami.assert_called_once_with("c", "mhcphase1", include_private=False) self._bake.hc_option.assert_called_once_with(ANY, "phase1_ami_name") def test_get_phase1_ami_id_with_priv_ami(self): '''Test that get_phase1_ami_id uses find_ami properly on success and excludes private ami''' def create_mock_ami(self, ami_id, name, stage=None, product_line=None, state=u'available', is_private=False): """Create mock ami with fixed id""" ami = self.mock_ami(name, stage, product_line, state, is_private) ami.id = ami_id return ami amis = [] amis.append( create_mock_ami(self, 'ami-abc001', 'mhcphase1 1', 'tested', 'astro', 'unavailable')) amis.append( create_mock_ami(self, 'ami-abc002', 'mhcphase1 2', 'tested')) amis.append( create_mock_ami(self, 'ami-abc003', 'mhcphase1 3', 'tested', is_private=True)) amis.append( create_mock_ami(self, 'ami-abc004', 'mhcphase1 4', 'tested', 'astro', is_private=True)) amis.append( create_mock_ami(self, 'ami-abc005', 'mhcphase1 5', 'failed')) self._bake.get_amis = MagicMock(return_value=amis) self._bake.hc_option = Mock(return_value="mhcphase1") self.assertEqual("ami-abc002", self._bake._get_phase1_ami_id(hostclass="mhcfoo")) def test_get_phase1_ami_id_raises(self): '''Test that get_phase1_ami_id raises AMIError if find_ami returns None''' self._bake.find_ami = Mock(return_value=None) self.assertRaises(AMIError, self._bake._get_phase1_ami_id, "mhcntp") def test_list_amis(self): '''Test that list amis can be called without filter successfully''' self.assertEqual(self._bake.list_amis(), self._amis) def test_list_amis_by_product_line(self): '''Test that list amis can filter by product line successfully''' self.assertEqual(self._bake.list_amis(product_line="astro"), [ self._amis_by_name["mhcfoo 0000000001"], self._amis_by_name["mhcfoo 0000000004"] ]) def test_list_amis_by_stage(self): '''Test that list amis can filter by stage successfully''' self.assertEqual(self._bake.list_amis(stage="failed"), [self._amis_by_name["mhcfoo 0000000005"]]) def test_list_amis_by_state(self): '''Test that list amis can filter by state successfully''' self.assertEqual(self._bake.list_amis(state="unavailable"), [ self._amis_by_name["mhcfoo 0000000001"], self._amis_by_name["mhcbar 0000000001"] ]) def test_list_amis_by_hostclass(self): '''Test that list amis can filter by hostclass successfully''' self.assertEqual(self._bake.list_amis(hostclass="mhcfoo"), [ self._amis_by_name["mhcfoo 0000000001"], self._amis_by_name["mhcfoo 0000000004"], self._amis_by_name["mhcfoo 0000000005"] ]) def test_list_amis_by_productline_and_stage(self): '''Test that list amis can filter by productline and stage successfully''' self.assertEqual( self._bake.list_amis(stage="tested", product_line="someone_else"), [self._amis_by_name["mhcbar 0000000001"]]) def test_cleanup_amis(self): '''Test that cleanup deletes AMIs''' self._bake.cleanup_amis(None, None, 'tested', -1, 0, False, None) for ami in self._amis: print ami.name, ami.id, ami.tags.get( 'stage'), ami.deregister.called self.assertTrue( self._amis_by_name["mhcbar 0000000001"].deregister.called) self.assertTrue( self._amis_by_name["mhcbar 0000000002"].deregister.called) self.assertTrue( self._amis_by_name["mhcfoo 0000000004"].deregister.called) def test_cleanup_amis_exclude(self): '''Test that cleanup ignores excluded AMIs''' self._bake.cleanup_amis(None, None, 'tested', -1, 0, False, [self._amis_by_name["mhcbar 0000000002"].id]) for ami in self._amis: print ami.name, ami.id, ami.tags.get( 'stage'), ami.deregister.called self.assertTrue( self._amis_by_name["mhcbar 0000000001"].deregister.called) self.assertFalse( self._amis_by_name["mhcbar 0000000002"].deregister.called) self.assertTrue( self._amis_by_name["mhcfoo 0000000004"].deregister.called) @patch('getpass.getuser', MagicMock(return_value="mock_user")) @patch('disco_aws_automation.DiscoBake._tag_ami') def test_extra_tags(self, mock_tag_ami): '''Test that additional tags are applied to AMI if specified''' ami = self._amis_by_name["mhcbar 0000000001"] self._bake._tag_ami_with_metadata(ami=ami, hostclass="mhcbar", source_ami_id='mock_source', stage='mock_stage', productline='mock_productline', extra_tags={'mock': 'gecko'}) mock_tag_ami.assert_called_once_with( ami, { "source_ami": "mock_source", "hostclass": "mhcbar", "stage": "mock_stage", "productline": "mock_productline", "is_private": "False", "baker": "mock_user", "version-asiaq": DiscoBake._git_ref(), "mock": "gecko" }) @patch('getpass.getuser', MagicMock(return_value="mock_user")) @patch('disco_aws_automation.DiscoBake._tag_ami') def test_extra_tags_no_override(self, mock_tag_ami): '''Test that additional tags do not override asiaq tags''' ami = self._amis_by_name["mhcbar 0000000001"] self._bake._tag_ami_with_metadata(ami=ami, hostclass="mhcbar", source_ami_id='mock_source', stage='mock_stage', productline='mock_productline', extra_tags={ 'mock': 'gecko', 'baker': 'innocent_user' }) mock_tag_ami.assert_called_once_with( ami, { "source_ami": "mock_source", "hostclass": "mhcbar", "stage": "mock_stage", "productline": "mock_productline", "is_private": "False", "baker": "mock_user", "version-asiaq": DiscoBake._git_ref(), "mock": "gecko" }) @patch('getpass.getuser', MagicMock(return_value="mock_user")) @patch('disco_aws_automation.DiscoBake._tag_ami') def test_is_private_tag(self, mock_tag_ami): '''Test that additional tags are applied to AMI if specified''' ami = self._amis_by_name["mhcbar 0000000001"] self._bake._tag_ami_with_metadata(ami=ami, hostclass="mhcbar", source_ami_id='mock_source', stage='mock_stage', productline='mock_productline', is_private=True, extra_tags={'mock': 'gecko'}) mock_tag_ami.assert_called_once_with( ami, { "source_ami": "mock_source", "hostclass": "mhcbar", "stage": "mock_stage", "productline": "mock_productline", "is_private": "True", "baker": "mock_user", "version-asiaq": DiscoBake._git_ref(), "mock": "gecko" }) def test_ami_filter_exclude_private(self): """Test ami_filter when excluding private AMIs""" amis = [] amis.append(self.mock_ami('mhcfoo 1', 'tested', 'astro', 'unavailable')) amis.append(self.mock_ami('mhcfoo 2', 'tested')) amis.append(self.mock_ami('mhcfoo 3', 'tested', is_private=True)) amis.append( self.mock_ami('mhcfoo 4', 'tested', 'astro', is_private=True)) amis.append(self.mock_ami('mhcfoo 5', 'failed')) self.assertEqual( self._bake.ami_filter(amis, 'tested', include_private=False), amis[0:2]) def test_ami_filter_include_private(self): """Test ami_filter when including private AMIs""" amis = [] amis.append(self.mock_ami('mhcfoo 1', 'tested', 'astro', 'unavailable')) amis.append(self.mock_ami('mhcfoo 2', 'tested')) amis.append(self.mock_ami('mhcfoo 3', 'tested', is_private=True)) amis.append( self.mock_ami('mhcfoo 4', 'tested', 'astro', is_private=True)) amis.append(self.mock_ami('mhcfoo 5', 'failed')) self.assertEqual(self._bake.ami_filter(amis, 'tested'), amis[:-1]) def test_find_ami_exclude_private(self): """Test find_ami when excluding private AMIs""" amis = [] amis.append(self.mock_ami('mhcfoo 1', 'tested', 'astro', 'unavailable')) amis.append(self.mock_ami('mhcfoo 2', 'tested')) amis.append(self.mock_ami('mhcfoo 3', 'tested', is_private=True)) amis.append( self.mock_ami('mhcfoo 4', 'tested', 'astro', is_private=True)) amis.append(self.mock_ami('mhcfoo 5', 'failed')) self._bake.get_amis = MagicMock(return_value=amis) self.assertEqual( self._bake.find_ami('tested', hostclass='mhcfoo', include_private=False), amis[1]) def test_find_ami_include_private(self): """Test find_ami when including private AMIs""" amis = [] amis.append(self.mock_ami('mhcfoo 1', 'tested', 'astro', 'unavailable')) amis.append(self.mock_ami('mhcfoo 2', 'tested')) amis.append(self.mock_ami('mhcfoo 3', 'tested', is_private=True)) amis.append( self.mock_ami('mhcfoo 4', 'tested', 'astro', is_private=True)) amis.append(self.mock_ami('mhcfoo 5', 'failed')) self._bake.get_amis = MagicMock(return_value=amis) self.assertEqual( self._bake.find_ami('tested', hostclass='mhcfoo', include_private=True), amis[3]) def test_promote_latest_ami_to_production(self): """Test promote_latest_ami_to_production is correct""" def create_mock_ami(self, name, stage=None, product_line=None, state=u'available', is_private=False): """Create mock ami with mock call for set_launch_permissions""" ami = self.mock_ami(name, stage, product_line, state, is_private) ami.set_launch_permissions = MagicMock(return_value=MagicMock()) ami.add_tags = MagicMock() return ami amis = [] amis.append( create_mock_ami(self, 'mhcfoo 1', 'tested', 'astro', 'unavailable')) amis.append(create_mock_ami(self, 'mhcfoo 2', 'tested')) amis.append( create_mock_ami(self, 'mhcfoo 3', 'tested', is_private=True)) amis.append( create_mock_ami(self, 'mhcfoo 4', 'tested', 'astro', is_private=True)) amis.append(create_mock_ami(self, 'mhcfoo 5', 'failed')) self._bake.get_amis = MagicMock(return_value=amis) self._bake.option = MagicMock(return_value="MockAccount") self._bake.promote_latest_ami_to_production("mhcfoo") amis[1].set_launch_permissions.assert_called_once_with("MockAccount") amis[0].set_launch_permissions.assert_not_called() amis[2].set_launch_permissions.assert_not_called() amis[1].add_tags.assert_called_once_with( {'shared_with_account_ids': 'MockAccount'}) def test_list_stragglers(self): """Test list_stragglers is correct when some amis are private""" amis = [] amis.append( self.mock_ami('mhcfoo 1', 'untested', 'astro', 'unavailable')) amis.append(self.mock_ami('mhcfoo 2', 'untested')) amis.append(self.mock_ami('mhcfoo 3', 'untested', is_private=True)) amis.append( self.mock_ami('mhcfoo 4', 'untested', 'astro', is_private=True)) self._bake.get_amis = MagicMock(return_value=amis) self.assertEqual(self._bake.list_stragglers(), {"mhcfoo": amis[1]})
def run(): """Parses command line and dispatches the commands""" config = read_config() args = docopt(__doc__) configure_logging(args["--debug"]) env = args["--environment"] or config.get("disco_aws", "default_environment") pipeline_definition = [] if args["--pipeline"]: with open(args["--pipeline"], "r") as f: reader = csv.DictReader(f) pipeline_definition = [line for line in reader] aws = DiscoAWS(config, env) if config.has_option('test', 'env'): test_env = config.get('test', 'env') test_aws = DiscoAWS(config, test_env) else: test_aws = aws deploy = DiscoDeploy(aws, test_aws, DiscoBake(config, aws.connection), pipeline_definition=pipeline_definition, test_hostclass=aws.config('hostclass', 'test'), test_user=aws.config('user', 'test'), test_command=aws.config('command', 'test'), ami=args.get("--ami"), hostclass=args.get("--hostclass"), allow_any_hostclass=args["--allow-any-hostclass"]) if args["test"]: deploy.test(dry_run=args["--dry-run"]) elif args["update"]: deploy.update(dry_run=args["--dry-run"]) elif args["list"]: missing = "-" if len(pipeline_definition) else "" if args["--tested"]: for (_hostclass, ami) in deploy.get_latest_tested_amis().iteritems(): print("{} {:40} {}".format( ami.id, ami.name.split()[0], deploy.get_integration_test(ami.name.split()[0]) or missing)) elif args["--untested"]: for (_hostclass, ami) in deploy.get_latest_untested_amis().iteritems(): print("{} {:40} {}".format( ami.id, ami.name.split()[0], deploy.get_integration_test(ami.name.split()[0]) or missing)) elif args["--failed"]: for (_hostclass, ami) in deploy.get_latest_failed_amis().iteritems(): print("{} {:40} {}".format( ami.id, ami.name.split()[0], deploy.get_integration_test(ami.name.split()[0]) or missing)) elif args["--testable"]: for ami in deploy.get_test_amis(): print("{} {:40} {}".format( ami.id, ami.name.split()[0], deploy.get_integration_test(ami.name.split()[0]) or missing)) elif args["--updatable"]: for ami in deploy.get_update_amis(): print("{} {:40} {}".format( ami.id, ami.name.split()[0], deploy.get_integration_test(ami.name.split()[0]) or missing)) elif args["--failures"]: failures = deploy.get_failed_amis() for ami in failures: print("{} {:40} {}".format( ami.id, ami.name.split()[0], deploy.get_integration_test(ami.name.split()[0]) or missing)) sys.exit(1 if len(failures) else 0)
def _special_date(ami): return (None if ami.name == 'mhcfoo 4' else DiscoBake.extract_ami_creation_time_from_ami_name(ami))
def run(): """Parses command line and dispatches the commands""" parser = get_parser() args = parser.parse_args() configure_logging(args.debug) if args.mode == "bake": bakery = DiscoBake(use_local_ip=args.use_local_ip) bakery.bake_ami(args.hostclass, args.no_destroy, args.source_ami, args.stage) elif args.mode == "create": HostclassTemplating.create_hostclass(args.hostclass) elif args.mode == "promote": bakery = DiscoBake() ami = bakery.get_image(args.ami) bakery.promote_ami(ami, args.stage) if args.promote_to_prod: bakery.promote_ami_to_production(ami) elif args.mode == "hostclasspromote": bakery = DiscoBake() bakery.promote_latest_ami_to_production(args.hostclass) elif args.mode == "listrepo": bakery = DiscoBake() repo = bakery.repo_instance() if repo: print(repo.ip_address) elif args.mode == "listamis": ami_ids = [args.ami] if args.ami else None instance_ids = [args.instance] if args.instance else None bakery = DiscoBake() amis = sorted(bakery.list_amis(ami_ids, instance_ids, args.stage, args.product_line), key=bakery.ami_timestamp) now = datetime.utcnow() for ami in amis: bakery.pretty_print_ami(ami, now, in_prod=args.in_prod) if not amis: sys.exit(1) elif args.mode == "liststragglers": bakery = DiscoBake() for hostclass, image in bakery.list_stragglers(args.days).iteritems(): print("{0}\t{1}".format(hostclass, image.id if image else '-')) elif args.mode == "listlatestami": bakery = DiscoBake() ami = bakery.find_ami(args.stage, args.hostclass) if ami: bakery.pretty_print_ami(ami) else: sys.exit(1) elif args.mode == "deleteami": bakery = DiscoBake() bakery.delete_ami(args.ami) elif args.mode == "cleanupamis": bakery = DiscoBake() bakery.cleanup_amis(args.hostclass, args.product_line, args.stage, args.days, args.count, args.dryrun)
class DiscoBakeTests(TestCase): '''Test DiscoBake class''' def mock_ami(self, name, stage=None, product_line=None, state=u'available'): '''Create a mock AMI''' def _mock_get(tag_name, default=None): if tag_name == "productline": return product_line if product_line else default if tag_name == "stage": return stage if stage else default ami = create_autospec(boto.ec2.image.Image) ami.name = name ami.tags = MagicMock(get=_mock_get) ami.id = 'ami-' + ''.join(random.choice("0123456789abcdef") for _ in range(8)) ami.state = state return ami def add_ami(self, name, stage, product_line=None, state=u'available'): '''Add one Instance AMI Mock to an AMI list''' ami = self.mock_ami(name, stage, product_line, state) assert ami.name == name assert ami.tags.get("stage") == stage assert ami.tags.get("productline") == product_line self._amis.append(ami) self._amis_by_name[ami.name] = ami return ami def setUp(self): self._bake = DiscoBake(config=MagicMock(), connection=MagicMock()) self._bake.promote_ami = MagicMock() self._bake.ami_stages = MagicMock(return_value=['untested', 'failed', 'tested']) self._bake.get_ami_creation_time = DiscoBake.extract_ami_creation_time_from_ami_name self._amis = [] self._amis_by_name = {} self.add_ami('mhcfoo 1', 'untested', 'astro') self.add_ami('mhcbar 2', 'tested') self.add_ami('mhcfoo 4', 'tested', 'astro') self.add_ami('mhcfoo 5', 'failed') self.add_ami('mhcbar 1', 'tested', 'someone_else') self._bake.get_amis = MagicMock(return_value=self._amis) def test_get_phase1_ami_id_success(self): '''Test that get_phase1_ami_id uses find_ami properly on success''' ami = Mock() type(ami).id = PropertyMock(return_value='ami-abcd1234') self._bake.ami_stages = Mock(return_value=['a', 'b', 'c']) self._bake.find_ami = Mock(return_value=ami) self._bake.hc_option = Mock(return_value="mhcphase1") self.assertEqual("ami-abcd1234", self._bake._get_phase1_ami_id(hostclass="mhcntp")) self._bake.find_ami.assert_called_once_with("c", "mhcphase1") self._bake.hc_option.assert_called_once_with(ANY, "phase1_ami_name") def test_get_phase1_ami_id_raises(self): '''Test that get_phase1_ami_id raises AMIError if find_ami returns None''' self._bake.find_ami = Mock(return_value=None) self.assertRaises(AMIError, self._bake._get_phase1_ami_id, "mhcntp") def test_list_amis(self): '''Test that list amis can be called without filter successfully''' self.assertEqual(self._bake.list_amis(), self._amis) def test_list_amis_by_product_line(self): '''Test that list amis can filter by product line successfully''' self.assertEqual( self._bake.list_amis(product_line="astro"), [ self._amis_by_name["mhcfoo 1"], self._amis_by_name["mhcfoo 4"]]) def test_list_amis_by_stage(self): '''Test that list amis can filter by stage successfully''' self.assertEqual(self._bake.list_amis(stage="failed"), [self._amis_by_name["mhcfoo 5"]]) def test_list_amis_by_productline_and_stage(self): '''Test that list amis can filter by productline and stage successfully''' self.assertEqual(self._bake.list_amis(stage="tested", product_line="someone_else"), [self._amis_by_name["mhcbar 1"]])