1
# Python
2 6
import json
3 6
import yaml
4 6
import logging
5 6
import os
6 6
import re
7 6
import subprocess
8 6
import six
9 6
from itertools import chain
10 6
from functools import reduce
11 6
from io import StringIO
12

13
# Django
14 6
from django.conf import settings
15 6
from django.core.exceptions import ObjectDoesNotExist
16 6
from django.db import DatabaseError
17 6
from django.utils.translation import ugettext_lazy as _
18 6
from django.db.models.fields.related import ForeignObjectRel, ManyToManyField
19 6
from django.db.models.query import QuerySet
20 6
from django.db.models import Q
21

22
# Django database
23 6
from django.db.migrations.loader import MigrationLoader
24 6
from django.db import connection
25

26
# Django REST Framework
27 6
from rest_framework.exceptions import ParseError, PermissionDenied
28 6
from django.utils.encoding import smart_str
29

30

31 6
logger = logging.getLogger('cyborgbackup.main.utils')
32

33 6
__all__ = ['get_object_or_400', 'get_object_or_403', 'to_python_boolean', 'get_module_provider',
34
           'camelcase_to_underscore', 'get_type_for_model', 'get_model_for_type',
35
           'timestamp_apiformat', 'getattrd', 'has_model_field_prefetched', 'get_all_field_names',
36
           'prefetch_page_capabilities', 'copy_model_by_class', 'copy_m2m_relationships',
37
           'get_cyborgbackup_version', 'get_search_fields', 'could_be_script',
38
           'model_instance_diff', 'model_to_dict', 'OutputEventFilter', 'get_ssh_version']
39

40

41 6
def get_module_provider():
42 6
    import importlib
43 6
    import importlib.util
44 6
    import pkgutil
45 6
    importlib.invalidate_caches()
46 6
    provider_dir = settings.PROVIDER_DIR
47 6
    data = []
48 6
    if os.path.isdir(provider_dir):
49 0
        for p in os.listdir(provider_dir):
50 0
            try:
51 0
                if os.path.isfile(os.path.join(provider_dir, p, '__init__.py')):
52 0
                    spec = importlib.util.spec_from_file_location(p, os.path.join(provider_dir, p, '__init__.py'))
53 0
                    loadedmodule = importlib.util.module_from_spec(spec)
54 0
                    spec.loader.exec_module(loadedmodule)
55 0
                    if hasattr(loadedmodule, 'module_name') and hasattr(loadedmodule, 'module_type'):
56 0
                        if loadedmodule.module_type() == 'vm':
57 0
                            module_name = loadedmodule.module_name()
58 0
                            extra_vars = ''
59 0
                            if hasattr(loadedmodule, 'module_extra_vars'):
60 0
                                extra_vars = loadedmodule.module_extra_vars()
61 0
                            data.append({'module': p, 'name': module_name, 'extra_vars': extra_vars})
62 0
                    del loadedmodule
63 0
            except Exception:
64 0
                pass
65 6
    del importlib
66 6
    del pkgutil
67 6
    return data
68

69

70 6
def load_module_provider(name):
71 0
    import importlib
72 0
    import importlib.util
73 0
    import pkgutil
74 0
    importlib.invalidate_caches()
75 0
    provider_dir = settings.PROVIDER_DIR
76 0
    the_module = None
77 0
    try:
78 0
        logger.debug(os.path.isfile(os.path.join(provider_dir, name, '__init__.py')))
79 0
        if os.path.isfile(os.path.join(provider_dir, name, '__init__.py')):
80 0
            spec = importlib.util.spec_from_file_location(name, os.path.join(provider_dir, name, '__init__.py'))
81 0
            loadedmodule = importlib.util.module_from_spec(spec)
82 0
            spec.loader.exec_module(loadedmodule)
83 0
            if hasattr(loadedmodule, 'module_name') and hasattr(loadedmodule, 'module_type'):
84 0
                if loadedmodule.module_type() == 'vm':
85 0
                    the_module = loadedmodule
86 0
    except Exception as e:
87 0
        logger.debug(e)
88 0
        pass
89 0
    del importlib
90 0
    del pkgutil
91 0
    return the_module
92

93

94 6
def model_instance_diff(old, new, serializer_mapping=None):
95
    """
96
    Calculate the differences between two model instances. One of the instances may be None (i.e., a newly
97
    created model or deleted model). This will cause all fields with a value to have changed (from None).
98
    serializer_mapping are used to determine read-only fields.
99
    When provided, read-only fields will not be included in the resulting dictionary
100
    """
101 6
    from django.db.models import Model
102

103 6
    if not(old is None or isinstance(old, Model)):
104 0
        raise TypeError('The supplied old instance is not a valid model instance.')
105 6
    if not(new is None or isinstance(new, Model)):
106 0
        raise TypeError('The supplied new instance is not a valid model instance.')
107 6
    old_password_fields = set(getattr(type(old), 'PASSWORD_FIELDS', [])) | set(['password'])
108 6
    new_password_fields = set(getattr(type(new), 'PASSWORD_FIELDS', [])) | set(['password'])
109

110 6
    diff = {}
111

112 6
    allowed_fields = get_allowed_fields(new, serializer_mapping)
113

114 6
    for field in allowed_fields:
115 6
        old_value = getattr(old, field, None)
116 6
        new_value = getattr(new, field, None)
117 6
        if old_value != new_value:
118 6
            diff[field] = (
119
                _convert_model_field_for_display(old, field, password_fields=old_password_fields),
120
                _convert_model_field_for_display(new, field, password_fields=new_password_fields),
121
            )
122

123 6
    if len(diff) == 0:
124 0
        diff = None
125

126 6
    return diff
127

128

129 6
def get_allowed_fields(obj, serializer_mapping):
130

131 6
    if serializer_mapping is not None and obj.__class__ in serializer_mapping:
132 6
        serializer_actual = serializer_mapping[obj.__class__]()
133 6
        allowed_fields = [x for x in serializer_actual.fields if not serializer_actual.fields[x].read_only] + ['id']
134
    else:
135 6
        allowed_fields = [x.name for x in obj._meta.fields]
136 6
    if obj._meta.model_name == 'user':
137 0
        field_blacklist = ['last_login']
138 0
        allowed_fields = [f for f in allowed_fields if f not in field_blacklist]
139 6
    return allowed_fields
140

141

142 6
def _convert_model_field_for_display(obj, field_name, password_fields=None):
143
    # NOTE: Careful modifying the value of field_val, as it could modify
144
    # underlying model object field value also.
145 6
    try:
146 6
        field_val = getattr(obj, field_name, None)
147 0
    except ObjectDoesNotExist:
148 0
        return '<missing {}>-{}'.format(obj._meta.verbose_name, getattr(obj, '{}_id'.format(field_name)))
149 6
    if password_fields is None:
150 0
        password_fields = set(getattr(type(obj), 'PASSWORD_FIELDS', [])) | set(['password'])
151 6
    if field_name in password_fields or (
152
            isinstance(field_val, six.string_types) and
153
            field_val.startswith('$encrypted$')
154
    ):
155 0
        return u'hidden'
156 6
    if hasattr(obj, 'display_%s' % field_name):
157 0
        field_val = getattr(obj, 'display_%s' % field_name)()
158 6
    if isinstance(field_val, (list, dict)):
159 0
        try:
160 0
            field_val = json.dumps(field_val, ensure_ascii=False)
161 0
        except Exception:
162 0
            pass
163 6
    if type(field_val) not in (bool, int, type(None)):
164 6
        field_val = smart_str(field_val)
165 6
    return field_val
166

167

168 6
def model_to_dict(obj, serializer_mapping=None):
169
    """
170
    Serialize a model instance to a dictionary as best as possible
171
    serializer_mapping are used to determine read-only fields.
172
    When provided, read-only fields will not be included in the resulting dictionary
173
    """
174 6
    password_fields = set(getattr(type(obj), 'PASSWORD_FIELDS', [])) | set(['password'])
175 6
    attr_d = {}
176

177 6
    allowed_fields = get_allowed_fields(obj, serializer_mapping)
178

179 6
    for field in obj._meta.fields:
180 6
        if field.name not in allowed_fields:
181 6
            continue
182 6
        attr_d[field.name] = _convert_model_field_for_display(obj, field.name, password_fields=password_fields)
183

184 6
    return attr_d
185

186

187 6
def get_object_or_400(klass, *args, **kwargs):
188
    '''
189
    Return a single object from the given model or queryset based on the query
190
    params, otherwise raise an exception that will return in a 400 response.
191
    '''
192 0
    from django.shortcuts import _get_queryset
193 0
    queryset = _get_queryset(klass)
194 0
    try:
195 0
        return queryset.get(*args, **kwargs)
196 0
    except queryset.model.DoesNotExist as e:
197 0
        raise ParseError(*e.args)
198 0
    except queryset.model.MultipleObjectsReturned as e:
199 0
        raise ParseError(*e.args)
200

201

202 6
def get_object_or_403(klass, *args, **kwargs):
203
    '''
204
    Return a single object from the given model or queryset based on the query
205
    params, otherwise raise an exception that will return in a 403 response.
206
    '''
207 0
    from django.shortcuts import _get_queryset
208 0
    queryset = _get_queryset(klass)
209 0
    try:
210 0
        return queryset.get(*args, **kwargs)
211 0
    except queryset.model.DoesNotExist as e:
212 0
        raise PermissionDenied(*e.args)
213 0
    except queryset.model.MultipleObjectsReturned as e:
214 0
        raise PermissionDenied(*e.args)
215

216

217 6
def to_python_boolean(value, allow_none=False):
218 0
    value = six.text_type(value)
219 0
    if value.lower() in ('true', '1', 't'):
220 0
        return True
221 0
    elif value.lower() in ('false', '0', 'f'):
222 0
        return False
223 0
    elif allow_none and value.lower() in ('none', 'null'):
224 0
        return None
225
    else:
226 0
        raise ValueError(_(u'Unable to convert "%s" to boolean') % six.text_type(value))
227

228

229 6
def camelcase_to_underscore(s):
230
    '''
231
    Convert CamelCase names to lowercase_with_underscore.
232
    '''
233 6
    s = re.sub(r'(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', '_\\1', s)
234 6
    return s.lower().strip('_')
235

236

237 6
def get_type_for_model(model):
238
    '''
239
    Return type name for a given model class.
240
    '''
241 6
    opts = model._meta.concrete_model._meta
242 6
    return camelcase_to_underscore(opts.object_name)
243

244

245 6
def get_all_field_names(model):
246
    # Implements compatibility with _meta.get_all_field_names
247
    # See: https://docs.djangoproject.com/en/1.11/ref/models/meta/#migrating-from-the-old-api
248 0
    return list(set(chain.from_iterable(
249
        (field.name, field.attname) if hasattr(field, 'attname') else (field.name,)
250
        for field in model._meta.get_fields()
251
        # For complete backwards compatibility, you may want to exclude
252
        # GenericForeignKey from the results.
253
        if not (field.many_to_one and field.related_model is None)
254
    )))
255

256

257 6
def get_search_fields(model):
258 6
    fields = []
259 6
    for field in model._meta.fields:
260 6
        if field.name in ('username', 'first_name', 'last_name', 'email',
261
                          'name', 'description'):
262 6
            fields.append(field.name)
263 6
    return fields
264

265

266 6
def validate_vars_type(vars_obj):
267 0
    if not isinstance(vars_obj, dict):
268 0
        vars_type = type(vars_obj)
269 0
        if hasattr(vars_type, '__name__'):
270 0
            data_type = vars_type.__name__
271
        else:
272 0
            data_type = str(vars_type)
273 0
        raise AssertionError(
274
            _('Input type `{data_type}` is not a dictionary').format(
275
                data_type=data_type)
276
        )
277

278

279 6
def parse_yaml_or_json(vars_str, silent_failure=True):
280
    '''
281
    Attempt to parse a string of variables.
282
    First, with JSON parser, if that fails, then with PyYAML.
283
    If both attempts fail, return an empty dictionary if `silent_failure`
284
    is True, re-raise combination error if `silent_failure` if False.
285
    '''
286 0
    if isinstance(vars_str, dict):
287 0
        return vars_str
288 0
    elif isinstance(vars_str, six.string_types) and vars_str == '""':
289 0
        return {}
290

291 0
    try:
292 0
        vars_dict = json.loads(vars_str)
293 0
        validate_vars_type(vars_dict)
294 0
    except (ValueError, TypeError, AssertionError) as json_err:
295 0
        try:
296 0
            vars_dict = yaml.safe_load(vars_str)
297
            # Can be None if '---'
298 0
            if vars_dict is None:
299 0
                return {}
300 0
            validate_vars_type(vars_dict)
301 0
        except (yaml.YAMLError, TypeError, AttributeError, AssertionError) as yaml_err:
302 0
            if silent_failure:
303 0
                return {}
304 0
            raise ParseError(_(
305
                'Cannot parse as JSON (error: {json_error}) or '
306
                'YAML (error: {yaml_error}).').format(
307
                    json_error=str(json_err), yaml_error=str(yaml_err)))
308 0
    return vars_dict
309

310

311 6
def get_cyborgbackup_version():
312
    '''
313
    Return CyBorgBackup version as reported by setuptools.
314
    '''
315 6
    from cyborgbackup import __version__
316 6
    try:
317 6
        import pkg_resources
318 6
        return pkg_resources.require('cyborgbackup')[0].version
319 6
    except Exception:
320 6
        return __version__
321

322

323 6
def get_cyborgbackup_migration_version():
324 0
    loader = MigrationLoader(connection, ignore_no_migrations=True)
325 0
    v = '000'
326 0
    for app_name, migration_name in loader.applied_migrations:
327 0
        if app_name == 'main':
328 0
            version_captures = re.findall('^[0-9]{4}_v([0-9]{3})_', migration_name)
329 0
            if len(version_captures) == 1:
330 0
                migration_version = version_captures[0]
331 0
                if migration_version > v:
332 0
                    v = migration_version
333 0
    return v
334

335

336 6
def filter_insights_api_response(json):
337 0
    new_json = {}
338
    '''
339
    'last_check_in',
340
    'reports.[].rule.severity',
341
    'reports.[].rule.description',
342
    'reports.[].rule.category',
343
    'reports.[].rule.summary',
344
    'reports.[].maintenance_actions.[].maintenance_plan.name',
345
    'reports.[].maintenance_actions.[].maintenance_plan.maintenance_id',
346
    '''
347

348 0
    if 'last_check_in' in json:
349 0
        new_json['last_check_in'] = json['last_check_in']
350 0
    if 'reports' in json:
351 0
        new_json['reports'] = []
352 0
        for rep in json['reports']:
353 0
            new_report = {
354
                'rule': {},
355
                'maintenance_actions': []
356
            }
357 0
            if 'rule' in rep:
358 0
                for k in ['severity', 'description', 'category', 'summary']:
359 0
                    if k in rep['rule']:
360 0
                        new_report['rule'][k] = rep['rule'][k]
361

362 0
            for action in rep.get('maintenance_actions', []):
363 0
                new_action = {'maintenance_plan': {}}
364 0
                if 'maintenance_plan' in action:
365 0
                    for k in ['name', 'maintenance_id']:
366 0
                        if k in action['maintenance_plan']:
367 0
                            new_action['maintenance_plan'][k] = action['maintenance_plan'][k]
368 0
                new_report['maintenance_actions'].append(new_action)
369

370 0
            new_json['reports'].append(new_report)
371 0
    return new_json
372

373

374 6
def get_model_for_type(type):
375
    '''
376
    Return model class for a given type name.
377
    '''
378 0
    from django.contrib.contenttypes.models import ContentType
379 0
    for ct in ContentType.objects.filter(Q(app_label='main') | Q(app_label='auth', model='user')):
380 0
        ct_model = ct.model_class()
381 0
        if not ct_model:
382 0
            continue
383 0
        ct_type = get_type_for_model(ct_model)
384 0
        if type == ct_type:
385 0
            return ct_model
386
    else:
387 0
        raise DatabaseError('"{}" is not a valid CyBorgBackup model.'.format(type))
388

389

390 6
def timestamp_apiformat(timestamp):
391 0
    timestamp = timestamp.isoformat()
392 0
    if timestamp.endswith('+00:00'):
393 0
        timestamp = timestamp[:-6] + 'Z'
394 0
    return timestamp
395

396

397 6
class NoDefaultProvided(object):
398 6
    pass
399

400

401 6
def getattrd(obj, name, default=NoDefaultProvided):
402
    """
403
    Same as getattr(), but allows dot notation lookup
404
    Discussed in:
405
    http://stackoverflow.com/questions/11975781
406
    """
407

408 0
    try:
409 0
        return reduce(getattr, name.split("."), obj)
410 0
    except AttributeError:
411 0
        if default != NoDefaultProvided:
412 0
            return default
413 0
        raise
414

415

416 6
def has_model_field_prefetched(model_obj, field_name):
417
    # NOTE: Update this function if django internal implementation changes.
418 0
    return getattr(getattr(model_obj, field_name, None),
419
                   'prefetch_cache_name', '') in getattr(model_obj, '_prefetched_objects_cache', {})
420

421

422 6
def prefetch_page_capabilities(model, page, prefetch_list, user):
423
    '''
424
    Given a `page` list of objects, a nested dictionary of user_capabilities
425
    are returned by id, ex.
426
    {
427
        4: {'edit': True, 'start': True},
428
        6: {'edit': False, 'start': False}
429
    }
430
    Each capability is produced for all items in the page in a single query
431

432
    Examples of prefetch language:
433
    prefetch_list = ['admin', 'execute']
434
      --> prefetch the admin (edit) and execute (start) permissions for
435
          items in list for current user
436
    prefetch_list = ['inventory.admin']
437
      --> prefetch the related inventory FK permissions for current user,
438
          and put it into the object's cache
439
    prefetch_list = [{'copy': ['inventory.admin', 'project.admin']}]
440
      --> prefetch logical combination of admin permission to inventory AND
441
          project, put into cache dictionary as "copy"
442
    '''
443 0
    page_ids = [obj.id for obj in page]
444 0
    mapping = {}
445 0
    for obj in page:
446 0
        mapping[obj.id] = {}
447

448 0
    for prefetch_entry in prefetch_list:
449

450 0
        display_method = None
451 0
        if type(prefetch_entry) is dict:
452 0
            display_method = prefetch_entry.keys()[0]
453 0
            paths = prefetch_entry[display_method]
454
        else:
455 0
            paths = prefetch_entry
456

457 0
        if type(paths) is not list:
458 0
            paths = [paths]
459

460
        # Build the query for accessible_objects according the user & role(s)
461 0
        filter_args = []
462 0
        for role_path in paths:
463 0
            if '.' in role_path:
464 0
                res_path = '__'.join(role_path.split('.')[:-1])
465 0
                role_type = role_path.split('.')[-1]
466 0
                parent_model = model
467 0
                for subpath in role_path.split('.')[:-1]:
468 0
                    parent_model = parent_model._meta.get_field(subpath).related_model
469 0
                filter_args.append(Q(
470
                    Q(**{'%s__pk__in' % res_path: parent_model.accessible_pk_qs(user, '%s_role' % role_type)}) |
471
                    Q(**{'%s__isnull' % res_path: True})))
472
            else:
473 0
                role_type = role_path
474 0
                filter_args.append(Q(**{'pk__in': model.accessible_pk_qs(user, '%s_role' % role_type)}))
475

476 0
        if display_method is None:
477
            # Role name translation to UI names for methods
478 0
            display_method = role_type
479 0
            if role_type == 'admin':
480 0
                display_method = 'edit'
481 0
            elif role_type in ['execute', 'update']:
482 0
                display_method = 'start'
483

484
        # Union that query with the list of items on page
485 0
        filter_args.append(Q(pk__in=page_ids))
486 0
        ids_with_role = set(model.objects.filter(*filter_args).values_list('pk', flat=True))
487

488
        # Save data item-by-item
489 0
        for obj in page:
490 0
            mapping[obj.pk][display_method] = bool(obj.pk in ids_with_role)
491

492 0
    return mapping
493

494

495 6
def copy_model_by_class(obj1, Class2, fields, kwargs):
496
    '''
497
    Creates a new unsaved object of type Class2 using the fields from obj1
498
    values in kwargs can override obj1
499
    '''
500 0
    create_kwargs = {}
501 0
    for field_name in fields:
502
        # Foreign keys can be specified as field_name or field_name_id.
503 0
        id_field_name = '%s_id' % field_name
504 0
        if hasattr(obj1, id_field_name):
505 0
            if field_name in kwargs:
506 0
                value = kwargs[field_name]
507 0
            elif id_field_name in kwargs:
508 0
                value = kwargs[id_field_name]
509
            else:
510 0
                value = getattr(obj1, id_field_name)
511 0
            if hasattr(value, 'id'):
512 0
                value = value.id
513 0
            create_kwargs[id_field_name] = value
514 0
        elif field_name in kwargs:
515 0
            if field_name == 'extra_vars' and isinstance(kwargs[field_name], dict):
516 0
                create_kwargs[field_name] = json.dumps(kwargs['extra_vars'])
517 0
            elif not isinstance(Class2._meta.get_field(field_name), (ForeignObjectRel, ManyToManyField)):
518 0
                create_kwargs[field_name] = kwargs[field_name]
519 0
        elif hasattr(obj1, field_name):
520 0
            field_obj = obj1._meta.get_field(field_name)
521 0
            if not isinstance(field_obj, ManyToManyField):
522 0
                create_kwargs[field_name] = getattr(obj1, field_name)
523

524
    # Apply class-specific extra processing for origination of jobs
525 0
    if hasattr(obj1, '_update_job_kwargs') and obj1.__class__ != Class2:
526 0
        new_kwargs = obj1._update_job_kwargs(create_kwargs, kwargs)
527
    else:
528 0
        new_kwargs = create_kwargs
529

530 0
    return Class2(**new_kwargs)
531

532

533 6
def copy_m2m_relationships(obj1, obj2, fields, kwargs=None):
534
    '''
535
    In-place operation.
536
    Given two saved objects, copies related objects from obj1
537
    to obj2 to field of same name, if field occurs in `fields`
538
    '''
539 0
    for field_name in fields:
540 0
        if hasattr(obj1, field_name):
541 0
            field_obj = obj1._meta.get_field(field_name)
542 0
            if isinstance(field_obj, ManyToManyField):
543
                # Many to Many can be specified as field_name
544 0
                src_field_value = getattr(obj1, field_name)
545 0
                if kwargs and field_name in kwargs:
546 0
                    override_field_val = kwargs[field_name]
547 0
                    if isinstance(override_field_val, (set, list, QuerySet)):
548 0
                        getattr(obj2, field_name).add(*override_field_val)
549 0
                        continue
550 0
                    if override_field_val.__class__.__name__ == 'ManyRelatedManager':
551 0
                        src_field_value = override_field_val
552 0
                dest_field = getattr(obj2, field_name)
553 0
                dest_field.add(*list(src_field_value.all().values_list('id', flat=True)))
554

555

556 6
def could_be_script(scripts_path, dir_path, filename):
557 0
    if os.path.splitext(filename)[-1] not in ['.py']:
558 0
        return None
559 0
    script_path = os.path.join(dir_path, filename)
560 0
    matchedLib = False
561 0
    matchedFunc = False
562 0
    try:
563 0
        for n, line in enumerate(open(script_path)):
564 0
            if 'from cyborgbackup.main.utils.params import Parameters' in line:
565 0
                matchedLib = True
566 0
            if 'def mainJob(' in line:
567 0
                matchedFunc = True
568 0
        source = open(script_path, 'r').read() + '\n'
569 0
        compile(source, filename, 'exec')
570 0
    except IOError:
571 0
        return None
572 0
    if not matchedLib and not matchedFunc:
573 0
        return None
574 0
    return os.path.relpath(script_path, smart_str(scripts_path))
575

576

577 6
class OutputEventFilter(object):
578
    '''
579
    File-like object that looks for encoded job events in stdout data.
580
    '''
581

582 6
    EVENT_DATA_RE = re.compile(r'\x1b\[K((?:[A-Za-z0-9+/=]+\x1b\[\d+D)+)\x1b\[K')
583

584 6
    def __init__(self, event_callback):
585 0
        self._event_callback = event_callback
586 0
        self._event_ct = 0
587 0
        self._counter = 1
588 0
        self._start_line = 0
589 0
        self._buffer = StringIO()
590 0
        self._last_chunk = ''
591 0
        self._current_event_data = None
592

593 6
    def flush(self):
594
        # pexpect wants to flush the file it writes to, but we're not
595
        # actually capturing stdout to a raw file; we're just
596
        # implementing a custom `write` method to discover and emit events from
597
        # the stdout stream
598 0
        pass
599

600 6
    def write(self, data):
601 0
        self._buffer.write(data)
602 0
        self._emit_event(data)
603 0
        self._buffer = StringIO()
604

605 6
    def close(self):
606 0
        value = self._buffer.getvalue()
607 0
        if value:
608 0
            self._emit_event(value)
609 0
            self._buffer = StringIO()
610 0
        self._event_callback(dict(event='EOF'))
611

612 6
    def _emit_event(self, buffered_stdout, next_event_data=None):
613 0
        next_event_data = next_event_data or {}
614 0
        if self._current_event_data:
615 0
            event_data = self._current_event_data
616 0
            stdout_chunks = [buffered_stdout]
617 0
        elif buffered_stdout:
618 0
            event_data = dict(event='verbose')
619 0
            stdout_chunks = buffered_stdout.splitlines(True)
620
        else:
621 0
            stdout_chunks = []
622

623 0
        for stdout_chunk in stdout_chunks:
624 0
            event_data['counter'] = self._counter
625 0
            self._counter += 1
626 0
            event_data['stdout'] = stdout_chunk[:-2] if len(stdout_chunk) > 2 else ""
627 0
            n_lines = stdout_chunk.count('\n')
628 0
            event_data['start_line'] = self._start_line
629 0
            event_data['end_line'] = self._start_line + n_lines
630 0
            self._start_line += n_lines
631 0
            if self._event_callback:
632 0
                self._event_callback(event_data)
633 0
                self._event_ct += 1
634

635 0
        if next_event_data.get('uuid', None):
636 0
            self._current_event_data = next_event_data
637
        else:
638 0
            self._current_event_data = None
639

640

641 6
def get_ssh_version():
642
    '''
643
    Return SSH version installed.
644
    '''
645 0
    try:
646 0
        proc = subprocess.Popen(['ssh', '-V'],
647
                                stderr=subprocess.PIPE)
648 0
        result = proc.communicate()[1].decode('utf-8')
649 0
        return result.split(" ")[0].split("_")[1]
650 0
    except Exception:
651 0
        return 'unknown'

Read our documentation on viewing source code .

Loading