Skip to content
Open
2 changes: 1 addition & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ source = apployer/
[report]
exclude_lines = def __repr__
# TODO set 100
fail_under = 91
fail_under = 92

# we won't work on those files and they will be eventually obsolete
omit =
Expand Down
2 changes: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=W1620,E1607,I0021,E1601,W1618,W1613,W1607,E1604,W1611,W1632,W1628,W1637,W1625,W1609,W1638,W1621,W1640,W1635,E1605,W1623,W0704,W1619,W1622,W1614,W1604,W1606,E1606,W1636,W1629,I0020,W1610,W1603,W1601,W1624,W1602,E1602,W1633,W1634,W1630,E1603,E1608,W1605,W1617,W1639,W1615,W1612,W1626,W1627,W1616,W1608,too-few-public-methods,fixme,locally-disabled,file-ignored
disable=too-few-public-methods,fixme,locally-disabled,file-ignored

[REPORTS]

Expand Down
165 changes: 165 additions & 0 deletions apployer/app_compare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#
# Copyright (c) 2016 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""
Comparing an application from appstack to its counterpart in the live environment.
"""

import logging
import pkg_resources

import datadiff
from requests.structures import CaseInsensitiveDict

from . import cf_api
from . import cf_cli

_log = logging.getLogger(__name__) # pylint: disable=invalid-name


def should_update(app):
"""Checks whether an application should be updated (or created from scratch) in the live
Cloud Foundry environment.

Args:
app (`apployer.appstack.AppConfig`): An application.

Returns:
bool: True if the app should be pushed, False otherwise.
"""
try:
app_guid = cf_cli.get_app_guid(app.name)
except cf_cli.CommandFailedError as ex:
_log.debug(str(ex))
_log.info("Failed to get GUID of app %s. Assuming it doesn't exist yet. "
"Will need to push it...", app.name)
return True
app_summary = cf_api.get_app_summary(app_guid)
app_properties = app.app_properties

appstack_version, live_env_version = _get_app_versions(app_properties, app_summary)
if appstack_version > live_env_version:
_log.info("Appstack's version of the app (%s) is higher than in the live env (%s). "
"Will update...", appstack_version, live_env_version)
return True
elif appstack_version < live_env_version:
_log.info("Appstack's version of the app (%s) is lower than in the live env (%s). "
"Won't push, because that would downgrade the app...",
appstack_version, live_env_version)
return False

return _properties_differ(app_properties, app_summary)


def _get_app_versions(app_properties, app_summary):
"""
Args:
app_properties (dict): Application's properties from appstack.
app_summary (dict): Application's properties from Cloud Foundry.

Returns:
(`pkg_resources.SetuptoolsVersion`, `pkg_resources.SetuptoolsVersion`): Tuple in form:
(app_version_in_appstack, app_version_in_live_env).
If version isn't found, then the lowest possible version will be returned.
"""
appstack_version = app_properties.get('env', {}).get('VERSION', '')
live_env_version = app_summary.get('environment_json', {}).get('VERSION', '')
# empty version string will be parsed to the lowest possible version
return (pkg_resources.parse_version(appstack_version),
pkg_resources.parse_version(live_env_version))


def _properties_differ(app_properties, app_summary):
"""Compares application's properties from appstack to those taken from a live environment.

Args:
app_properties (dict): Application's properties from appstack.
app_summary (dict): Application's properties from Cloud Foundry.

Returns:
bool: True if one of the properties present in `app_properties` is different in
`app_summary`. False otherwise.
"""
for key, value in app_properties.items():
if key == 'env':
if not _dict_is_part_of(app_summary['environment_json'], value):
_log.info("Differences in application's env:\n%s",
datadiff.diff(app_summary['environment_json'], value,
fromfile='live env', tofile='appstack'))
return True
elif key in ('disk_quota', 'memory'):
# Values in the manifest will be strings and have suffix M, MB, G or GB,
# while values in summary will be ints specifying the number of megabytes.
megabytes_in_properties = _normalize_to_megabytes(value)
if megabytes_in_properties != app_summary[key]:
_log.info("Difference in application's %s field: %s (live env) vs. %s (appstack).",
key, app_summary[key], megabytes_in_properties)
return True
elif key == 'services':
summary_services = [service['name'] for service in app_summary[key]]
if not set(value).issubset(set(summary_services)):
_log.info("Difference in application's services: \n%s",
datadiff.diff(app_summary[key], value,
fromfile='live env', tofile='appstack'))
return True
elif key == 'host':
summary_hosts = [route['host'] for route in app_summary['routes']]
if value not in summary_hosts:
_log.info("Application's hosts in live env don't contain %s.", value)
return True
else:
if value != app_summary[key]:
_log.info("Difference in application's %s field: %s (live env) vs. %s (appstack).",
key, app_summary[key], value)
return True
return False


def _dict_is_part_of(dict_a, dict_b):
"""
Checks whether dict_b is a part of dict_a.
That is if dict_b is dict_a, just with some (or none) keys removed.

Args:
dict_a (dict):
dict_b (dict):

Return:
bool: True if dict_a contains dict_b.
"""
dict_a, dict_b = CaseInsensitiveDict(dict_a), CaseInsensitiveDict(dict_b)
for key, value in dict_b.items():
if key not in dict_a or dict_a[key] != value:
return False
return True


def _normalize_to_megabytes(value):
"""
Args:
value (str): A string with a number and a size suffix, like 2GB or 300M.

Returns:
int: Number of megabytes represented by the `value`.
"""
if value.endswith('M'):
return int(value[:-1])
elif value.endswith('MB'):
return int(value[:-2])
elif value.endswith('G'):
return int(value[:-1]) * 1024
elif value.endswith('GB'):
return int(value[:-2]) * 1024
13 changes: 13 additions & 0 deletions apployer/cf_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def delete_service_binding(binding):
raise cf_cli.CommandFailedError('Failed to delete a service binding. CF response: {}'
.format(cmd_output))


def get_app_name(app_guid):
"""
Args:
Expand All @@ -78,6 +79,18 @@ def get_app_name(app_guid):
return app_desctiption['entity']['name']


def get_app_summary(app_guid):
"""Gets a summary of the application's state. This includes its full configuration.

Args:
app_guid (str): Application's GUID.

Returns:
dict: Application's summary. Contains fields like "services", "memory", "buildpack", etc.
"""
return _cf_curl_get('/v2/apps/{}/summary'.format(app_guid))


def get_upsi_credentials(service_guid):
"""Gets the credentials (configuration) of a user-provided service instance.

Expand Down
24 changes: 23 additions & 1 deletion apployer/cf_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def bind_service(app_name, instance_name):
def buildpacks():
"""
Returns:
list[`BuildpackDescription`]: A list of buildpacks on the environment.
list[`BuildpackDescription`]: A list of buildpacks in the environment.
"""
out = get_command_output([CF, 'buildpacks'])
buildpack_lines = out.splitlines()[3:]
Expand Down Expand Up @@ -216,6 +216,18 @@ def env(app_name):
return get_command_output([CF, 'env', app_name])


def get_app_guid(app_name):
"""
Args:
app_name (str): Name of a service instace (can be a user-provided one).

Returns:
str: GUID of the application that can be used in API calls.
"""
cmd_output = get_command_output([CF, 'app', '--guid', app_name])
return cmd_output.split()[0]


def get_service_guid(service_name):
"""
Args:
Expand Down Expand Up @@ -287,6 +299,16 @@ def service(service_name):
return get_command_output([CF, 'service', service_name])


def service_brokers():
"""
Returns:
set[str]: A set of brokers (their names) in the environment.
"""
out = get_command_output([CF, 'service-brokers'])
broker_lines = out.splitlines()[3:]
return set([line.split()[0] for line in broker_lines])


def update_buildpack(buildpack_name, buildpack_path):
"""Updates a buildpack.

Expand Down
Loading