Completed
Pull Request — master (#2836)
by Edward
06:37
created

DownloadGitRepoAction._is_desired_pack()   A

Complexity

Conditions 4

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
c 2
b 0
f 0
dl 0
loc 14
rs 9.2
1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
import os
17
import shutil
18
import hashlib
19
import stat
20
21
import six
22
from git.repo import Repo
23
from lockfile import LockFile
24
25
from st2actions.runners.pythonrunner import Action
26
from st2common.util.green import shell
27
28
MANIFEST_FILE = 'pack.yaml'
29
CONFIG_FILE = 'config.yaml'
30
GITINFO_FILE = '.gitinfo'
31
PACK_RESERVE_CHARACTER = '.'
32
33
PACK_GROUP_CFG_KEY = 'pack_group'
34
EXCHANGE_URL_KEY = 'exchange_url'
35
36
37
class DownloadGitRepoAction(Action):
38
    def __init__(self, config=None, action_service=None):
39
        super(DownloadGitRepoAction, self).__init__(config=config, action_service=action_service)
40
        self._subtree = None
41
        self._repo_url = None
42
43
    def run(self, name_or_url, ref, abs_repo_base, verifyssl=True, branch='master'):
44
45
        self._pack_name, self._pack_url = self._get_pack_name_and_url(
46
            name_or_url,
47
            self.config.get(EXCHANGE_URL_KEY, None)
48
        )
49
50
        lock_name = hashlib.md5(self._pack_name).hexdigest() + '.lock'
51
52
        with LockFile('/tmp/%s' % (lock_name)):
53
            abs_local_path = self._clone_repo(repo_name=self._pack_name, repo_url=self._pack_url,
54
                                              verifyssl=verifyssl, branch=ref)
55
            try:
56
                result = self._move_pack(abs_repo_base, self._pack_name, abs_local_path)
57
            finally:
58
                self._cleanup_repo(abs_local_path)
59
60
        return self._validate_result(result=result, repo_url=self._pack_url)
61
62
    @staticmethod
63
    def _clone_repo(repo_name, repo_url, verifyssl=True, branch='master'):
64
        user_home = os.path.expanduser('~')
65
        abs_local_path = os.path.join(user_home, repo_name)
66
67
        # Disable SSL cert checking if explictly asked
68
        if not verifyssl:
69
            os.environ['GIT_SSL_NO_VERIFY'] = 'true'
70
        # Shallow clone the repo to avoid getting all the metadata. We only need HEAD of a
71
        # specific branch so save some download time.
72
        Repo.clone_from(repo_url, abs_local_path, branch=branch, depth=1)
73
        return abs_local_path
74
75
    def _move_pack(self, abs_repo_base, pack_name, abs_local_path):
76
        result = {}
77
78
        desired, message = DownloadGitRepoAction._is_desired_pack(abs_local_path, pack_name)
79
        if desired:
80
            to = abs_repo_base
81
            dest_pack_path = os.path.join(abs_repo_base, pack_name)
82
            if os.path.exists(dest_pack_path):
83
                self.logger.debug('Removing existing pack %s in %s to replace.', pack_name,
84
                                  dest_pack_path)
85
86
                # Ensure to preserve any existing configuration
87
                old_config_file = os.path.join(dest_pack_path, CONFIG_FILE)
88
                new_config_file = os.path.join(abs_local_path, CONFIG_FILE)
89
90
                if os.path.isfile(old_config_file):
91
                    shutil.move(old_config_file, new_config_file)
92
93
                shutil.rmtree(dest_pack_path)
94
95
            self.logger.debug('Moving pack from %s to %s.', abs_local_path, to)
96
            shutil.move(abs_local_path, to)
97
            # post move fix all permissions.
98
            self._apply_pack_permissions(pack_path=dest_pack_path)
99
            message = 'Success.'
100
        elif message:
101
            message = 'Failure : %s' % message
102
103
        result[pack_name] = (desired, message)
104
105
        return result
106
107
    def _apply_pack_permissions(self, pack_path):
108
        """
109
        Will recursively apply permission 770 to pack and its contents.
110
        """
111
        # 1. switch owner group to configured group
112
        pack_group = self.config.get(PACK_GROUP_CFG_KEY, None)
113
        if pack_group:
114
            shell.run_command(['sudo', 'chgrp', '-R', pack_group, pack_path])
115
116
        # 2. Setup the right permissions and group ownership
117
        # These mask is same as mode = 775
118
        mode = stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH | stat.S_IXOTH
119
        os.chmod(pack_path, mode)
120
121
        # Yuck! Since os.chmod does not support chmod -R walk manually.
122
        for root, dirs, files in os.walk(pack_path):
123
            for d in dirs:
124
                os.chmod(os.path.join(root, d), mode)
125
            for f in files:
126
                os.chmod(os.path.join(root, f), mode)
127
128
    @staticmethod
129
    def _is_desired_pack(abs_pack_path, pack_name):
130
        # path has to exist.
131
        if not os.path.exists(abs_pack_path):
132
            return (False, 'Pack "%s" not found or it\'s missing a "pack.yaml" file.' %
133
                    (pack_name))
134
        # should not include reserve characters
135
        if PACK_RESERVE_CHARACTER in pack_name:
136
            return (False, 'Pack name "%s" contains reserve character "%s"' %
137
                    (pack_name, PACK_RESERVE_CHARACTER))
138
        # must contain a manifest file. Empty file is ok for now.
139
        if not os.path.isfile(os.path.join(abs_pack_path, MANIFEST_FILE)):
140
            return (False, 'Pack is missing a manifest file (%s).' % (MANIFEST_FILE))
141
        return (True, '')
142
143
    @staticmethod
144
    def _cleanup_repo(abs_local_path):
145
        # basic lock checking etc?
146
        if os.path.isdir(abs_local_path):
147
            shutil.rmtree(abs_local_path)
148
149
    @staticmethod
150
    def _validate_result(result, repo_url):
151
        atleast_one_success = False
152
        sanitized_result = {}
153
        for k, v in six.iteritems(result):
154
            atleast_one_success |= v[0]
155
            sanitized_result[k] = v[1]
156
157
        if not atleast_one_success:
158
            message_list = []
159
            message_list.append('The pack has not been downloaded from "%s".\n' % (repo_url))
160
            message_list.append('Errors:')
161
162
            for pack, value in result.items():
163
                success, error = value
164
                message_list.append(error)
165
166
            message = '\n'.join(message_list)
167
            raise Exception(message)
168
169
        return sanitized_result
170
171
    @staticmethod
172
    def _get_pack_name_and_url(name_or_url, exchange_url):
173
        if len(name_or_url.split('/')) == 1:
174
            return (name_or_url, "{}/{}.git".format(exchange_url, name_or_url))
175
        else:
176
            return (DownloadGitRepoAction._eval_repo_name(name_or_url),
177
                    DownloadGitRepoAction._eval_repo_url(name_or_url))
178
179
    @staticmethod
180
    def _eval_repo_url(repo_url):
181
        """Allow passing short GitHub style URLs"""
182
        if not repo_url:
183
            raise Exception('No valid repo_url provided or could be inferred.')
184
        has_git_extension = repo_url.endswith('.git')
185
        if len(repo_url.split('/')) == 2 and "git@" not in repo_url:
186
            url = "https://github.com/{}".format(repo_url)
187
        else:
188
            url = repo_url
189
        return url if has_git_extension else "{}.git".format(url)
190
191
    @staticmethod
192
    def _eval_repo_name(repo_url):
193
        """
194
        Evaluate the name of the repo.
195
        https://github.com/StackStorm/st2contrib.git -> st2contrib
196
        https://github.com/StackStorm/st2contrib -> st2contrib
197
        [email protected]:StackStorm/st2contrib.git -> st2contrib
198
        [email protected]:StackStorm/st2contrib -> st2contrib
199
        """
200
        last_forward_slash = repo_url.rfind('/')
201
        next_dot = repo_url.find('.', last_forward_slash)
202
        # If dot does not follow last_forward_slash return till the end
203
        if next_dot < last_forward_slash:
204
            return repo_url[last_forward_slash + 1:]
205
        return repo_url[last_forward_slash + 1:next_dot]
206