예제 #1
0
 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)
예제 #2
0
 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)
예제 #3
0
 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)
예제 #4
0
    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"
            })
예제 #5
0
 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)
예제 #6
0
    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"
            })
예제 #7
0
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)
예제 #8
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)
예제 #9
0
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)
예제 #10
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,
            "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)
예제 #11
0
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]})
예제 #12
0
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)
예제 #13
0
 def _special_date(ami):
     return (None if ami.name == 'mhcfoo 4' else
             DiscoBake.extract_ami_creation_time_from_ami_name(ami))
예제 #14
0
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)
예제 #15
0
 def _special_date(ami):
     return (None if ami.name == 'mhcfoo 4' else
             DiscoBake.extract_ami_creation_time_from_ami_name(ami))
예제 #16
0
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"]])