the f*ck rants about stuff

All Posts

Latest posts related to :



  1. How to clone a server using just rsync

    In the past I needed more space in the server and so i had to upgraded it to a more expensive option, without option of going back

    Now the basic server option is cheaper and is enough for me. Plus there were some black friday discounts :)

    So I decided to move the server with all my services to a cheaper option and save 75% of what i was spending with more or less the same features

    Unfortunately, this is not supported by default and theres no one button way to do it. Fortunately, this is very easy to do using linux!

    People fighting over products in black friday fashion

    This is how i did it in 6 easy steps:

    Step 1

    • Reboot booh machines using a live image and have a working ssh server on the target server
    • Mount the server disk on both servers on /mnt

    Step 2

    • rsync -AHXavP --numeric-ids --exclude='/mnt/dev' --exclude='/mnt/proc' --exclude='/mnt/sys' /mnt/ root@ip.dest.server:/mnt/

    Step 3

    • ssh on the target server. Bind /proc /dev /sys to /mnt/ and chroot it
    • grub-install /dev/sdb && update-grub
    • ack ip.orig.server /etc/ and change it where appropiate
    • reboot

    Step 4

    • Change DNS

    Step 5

    • ????

    Step 6

    • Profit!
    Conclusion
    A couple of hours to do the whole thing including buying the new server and everything seems to be working as if nothing happened. Copying directly from server to server helped with the downtime too. Aint linux wonderful?
    
  2. Get a nearly fresh debian install without reinstalling

    I was recently asked how to get rid of the old and unused packages without having to reinstall?

    Debian have the mechanisms to deal with this and more. Unfortunately for new people, its not as automated and a little more obscure that i would like

    Anyway, heres what i would do:

    # apt-mark showmanual
    # apt-mark auto <packages you dont recognize>
    # apt purge <packages you recognize but dont want anymore>
    # apt autoremove --purge
    
  3. On learning german: Difficulties

    Im having a great time learning german so far. Its a beautiful language with a lot of nuances!

    Maybe its too soon too speak, ive been only learning it for ~9 months now but the actual difficulties im facing are different of what i was expecting

    Before diving into it i thought that maybe the declensions were too difficult. But thats not the case. You might not decline perfectly, but with very little practice you can decline good enough to be honest

    Or the huge long-ass words. But once you recognize the words inside, this is not really a problem. Even if you dont know the meaning of every word inside, you know more or less where to cut it

    Or the gender of the nouns. But once I learned the gender along the word, i dont seem to forget it again 95% of the times. Also, although there are no rules, you can develop a sense of what the gender of a noun is even without knowing the meaning. I guess it right most of the time by now!

    The actual difficult things i found are:

    • Words dont translate 1 to 1. In the case of english-spanish translations, words that dont translate 1-1 are the exception more than the rule. So far i learned 7 words that more or less translate to “but/however”, 18 words for “to get/receive”, 79 for “to miss” and 124 words for “to happen/to occur”. Im just learning them all in a frecuency of use fashion. I hope to be able to grasp the nuances of use in the future ;)

    • The fact that german is a very nuanced language. They use actual words to convey meaning that other languages might convey via entonation or not convey at all. Basically you are very likely to lose a lot of nuanced meaning when translating from german!

    • Many many words look the same! Its not just one or a handful, its many many of them that are very similar with a different meaning

    Id say at least half the words i learned so far are like this

    If you dont believe me, just check it out. I made a list of dificult german words

    And these are only words that i know already (more or less) and 95% of them belong to the top 1000 more frequently used words. Many many more are missing!

  4. Automating the creation of an static website

    tl;dr:
    python can turn tedious work into free time!
    New website about hiking in Extremadura, Spain: extremaruta.es

    extremaruta website snapshot 1

    extremaruta website snapshot 2

    PHP websites were on the rise a few years ago, mainly due to the raise of easy CMS like drupal and joomla. Their main problem is that they carry a high maintenance cost with them compared with an static website. You have to keep them up to date and theres new exploits every other week

    I was presented with this PHP website that had been hacked very long ago and it had to be taken down because there was no way to clean it up and there was no clean copy anywhere. The only reason they were using a PHP website was that it was “easy” upfront but they never really think it throught and they didnt really needed anything dynamic, like users

    One of the perks of static websites is that they are virtually impossible to hack and in case they are (probably because something else has been hacked and it gets affected), you can have it up again somewhere else in a matter of minutes

    So off we go to turn the original data into a website. I chose my prefered static website generator, pelican, and then wrote a few python scripts that mostly spew markdown (so no not even pelican specific generator!)

    It scans a directory with photos, .gpx and .pdf and generates the markdown and figure out where they belong and whats part of the website by the name of the files

    The major challenge was to reduce times because theres almost 10Gb of data that have to processed and it would had been very tedious to debug otherwise. Thumbnails have to get generated, watermarks added, decide if something new has been added on the original data, etc… Anything done, has to undergo through 10Gb of data

    """
    process.py
    
        Move files around and triggers the different proccesses
        in the order it needs to run, both for testing and for production
    """
    
    #!/usr/bin/python3
    
    import routes
    from shutil import move
    from subprocess import run
    from os.path import join, exists
    
    
    def sync_files():
        orig = join(routes.OUTPUT, "")
        dest = join(routes.FINAL_OUTPUT, "")
        linkdest = join("..", "..", orig)
        command = ("rsync", "-ah", "--delete",
                   "--link-dest={}".format(linkdest), orig, dest)
        reallinkdest = join(dest, linkdest)
        if(exists(reallinkdest)):
            #print("{} exists".format(reallinkdest))
            run(command)
        else:
            print("{} doesnt exist".format(reallinkdest))
            print("its very likely the command is wrong:\n{}".format(command))
            exit(1)
    
    
    def test_run():
        f = '.files_cache_files'
        if(exists(f)):
            #move(f, 'todelete')
            pass
        r = routes.Routes("real.files")
        # print(r)
        r.move_files()
        r.generate_markdown()
    
        sync_files()
    
    
    def final_run():
        r = routes.Routes("/media/usb/web/")
        # print(routes)
        r.move_files()
        r.generate_markdown()
    
        sync_files()
    
    
    test_run()
    # final_run()
    
    #!/usr/bin/python3
    """
    routes.py
    
        Generate the different information and intermediate cache files so it doesnt
        have to process everything every time
    """
    
    try:
        from slugify import slugify
    except ImportError as e:
        print(e)
        print("Missing module. Please install python3-slugify")
        exit()
    
    from pprint import pformat
    from shutil import copy
    from os.path import join, exists, basename, splitext
    import os
    import re
    import json
    
    # original files path
    ORIG_BASE = "/media/usb0/web/"
    ORIG_BASE = "files"
    ORIG_BASE = "real.files"
    # relative dest to write content
    OUTPUT = join("content", "auto", "")
    # relative dest pdf and gpx
    STATIC = join("static", "")
    FULL_STATIC = join("auto", "static", "")
    # relative photos dest
    PHOTOS = join("photos", "")
    # relative markdown dest
    PAGES = join("rutas", "")
    # relative banner dest
    BANNER = join(PHOTOS, "banner", "")
    # absolute dests
    BASE_PAGES = join(OUTPUT, PAGES, "")
    BASE_STATIC = join(OUTPUT, STATIC, "")
    BASE_PHOTOS = join(OUTPUT, PHOTOS, "")
    BASE_BANNER = join(OUTPUT, BANNER, "")
    
    TAGS = 'tags.txt'
    
    # Where to copy everything once its generated
    FINAL_OUTPUT = join("web", OUTPUT)
    
    def hard_link(src, dst):
        """Tries to hard link and copy it instead where it fails"""
        try:
            os.link(src, dst)
        except OSError:
            copy(src, dst)
    
    def sanitize_name(fpath):
        """ returns sane file names: '/á/b/c áD.dS' -> c-ad.ds"""
        fname = basename(fpath)
        split_fname = splitext(fname)
        name = slugify(split_fname[0])
        ext = slugify(split_fname[1]).lower()
        return ".".join((name, ext))
    
    class Routes():
        pdf_re = re.compile(r".*/R(\d{1,2}).*(?:PDF|pdf)$")
        gpx_re = re.compile(r".*/R(\d{1,2}).*(?:GPX|gpx)$")
        jpg_re = re.compile(r".*/\d{1,2}R(\d{1,2}).*(?:jpg|JPG)$")
        banner_re = re.compile(r".*BANNER/Etiquetadas/.*(?:jpg|JPG)$")
    
        path_re = re.compile(r".*PROVINCIA DE (.*)/\d* (.*)\ (?:CC|BA)/.*")
    
        def __getitem__(self, item):
            return self.__routes__[item]
    
        def __iter__(self):
            return iter(self.__routes__)
    
        def __str__(self):
            return pformat(self.__routes__)
    
        def __init__(self, path):
            self.__routes__ = {}
            self.__files__ = {}
    
            self.fcache = ".files_cache_" + slugify(path)
    
            if(exists(self.fcache)):
                print(f"Using cache to read. {self.fcache} detected:")
                self._read_files_cache()
            else:
                print(f"No cache detected. Reading from {path}")
                self._read_files_to_cache(path)
    
        def _init_dir(self, path, create_ruta_dirs=True):
            """ create dir estructure. Returns True if it had to create"""
            created = True
    
            if(exists(path)):
                print(f"{path} exist. No need to create dirs")
                created = False
            else:
                print(f"{path} doesnt exist. Creating dirs")
                os.makedirs(path)
                if(create_ruta_dirs):
                    self._create_ruta_dirs(path)
    
            return created
    
        def _create_ruta_dirs(self, path):
            """Create structure of directories in <path>"""
            for prov in self.__routes__:
                prov_path = join(path, slugify(prov))
                if(not exists(prov_path)):
                    os.makedirs(prov_path)
                for comar in self.__routes__[prov]:
                    comar_path = join(prov_path, slugify(comar))
                    if(not exists(comar_path)):
                        os.makedirs(comar_path)
                    # Special case for BASE_PAGES. Dont make last ruta folder
                    if(path != BASE_PAGES):
                        for ruta in self.__routes__[prov].get(comar):
                            ruta_path = join(comar_path, ruta)
                            if(not exists(ruta_path)):
                                os.makedirs(ruta_path)
    
        def _read_files_cache(self):
            with open(self.fcache) as f:
                temp = json.load(f)
            self.__routes__ = temp['routes']
            self.__files__ = temp['files']
    
        def _read_files_to_cache(self, path):
            """read files from path into memory. Also writes the cache file"""
            """also read tags"""
            for root, subdirs, files in os.walk(path):
                for f in files:
    
                    def append_ruta_var(match, var_name):
                        prov, comar = self._get_prov_comar(root)
                        ruta = match.group(1).zfill(2)
                        var_path = join(root, f)
                        r = self._get_ruta(prov, comar, ruta)
                        r.update({var_name: var_path})
    
                    def append_ruta_pic(match):
                        prov, comar = self._get_prov_comar(root)
                        ruta = match.group(1).zfill(2)
                        pic_path = join(root, f)
                        r = self._get_ruta(prov, comar, ruta)
                        pics = r.setdefault('pics', list())
                        pics.append(pic_path)
    
                    def pdf(m):
                        append_ruta_var(m, 'pdf_orig')
    
                    def gpx(m):
                        append_ruta_var(m, 'gpx_orig')
    
                    def append_banner(m):
                        pic_path = join(root, f)
                        banner = self.__files__.setdefault('banner', list())
                        banner.append(pic_path)
    
                    regexes = (
                        (self.banner_re, append_banner),
                        (self.pdf_re, pdf),
                        (self.gpx_re, gpx),
                        (self.jpg_re, append_ruta_pic),
                    )
    
                    for reg, func in regexes:
                        try:
                            match = reg.match(join(root, f))
                            if(match):
                                func(match)
                                break
                            # else:
                            #    print(f"no match for {root}/{f}")
                        except Exception:
                            print(f"Not sure how to parse this file: {f}")
                            print(f"r: {root}\ns: {subdirs}\nf: {files}\n\n")
    
            self._read_tags()
    
            temp = dict({'routes': self.__routes__, 'files': self.__files__})
            with open(self.fcache, "w") as f:
                json.dump(temp, f)
    
        def _read_tags(self):
            with open(TAGS) as f:
                for line in f.readlines():
                    try:
                        ruta, short_name, long_name, tags = [
                            p.strip() for p in line.split(":")]
                        prov, comar, number, _ = ruta.split("/")
                        r = self._get_ruta(prov, comar, number)
                        r.update({'short': short_name})
                        r.update({'long': long_name})
                        final_tags = list()
                        for t in tags.split(","):
                            final_tags.append(t)
                        r.update({'tags': final_tags})
                    except ValueError:
                        pass
    
        def _get_prov_comar(self, path):
            pathm = self.path_re.match(path)
            prov = pathm.group(1)
            comar = pathm.group(2)
    
            return prov, comar
    
        def _get_ruta(self, prov, comar, ruta):
            """creates the intermeidate dics if needed"""
    
            prov = slugify(prov)
            comar = slugify(comar)
    
            p = self.__routes__.get(prov)
            if(not p):
                self.__routes__.update({prov: {}})
    
            c = self.__routes__.get(prov).get(comar)
            if(not c):
                self.__routes__.get(prov).update({comar: {}})
    
            r = self.__routes__.get(prov).get(comar).get(ruta)
            if(not r):
                self.__routes__.get(prov).get(comar).update({ruta: {}})
    
            r = self.__routes__.get(prov).get(comar).get(ruta)
            return r
    
        def move_files(self):
            """move misc (banner) and ruta related files (not markdown)"""
            """from dir to OUTPUT"""
            self._move_ruta_files()
            # misc have to be moved after ruta files, because the folder
            # inside photos prevents ruta photos to be moved
            self._move_misc_files()
    
        def _move_misc_files(self):
            if (self._init_dir(BASE_BANNER, False)):
                print("moving banner...")
    
                for f in self.__files__['banner']:
                    fname = basename(f)
                    dest = slugify(basename(f))
                    hard_link(f, join(BASE_BANNER, sanitize_name(f)))
    
        def _move_ruta_files(self):
            """move everything ruta related: static and photos(not markdown)"""
            create_static = False
            create_photos = False
    
            if (self._init_dir(BASE_STATIC)):
                print("moving static...")
                create_static = True
    
            if (self._init_dir(BASE_PHOTOS)):
                print("moving photos...")
                create_photos = True
    
            for prov in self.__routes__:
                for comar in self.__routes__[prov]:
                    for ruta in self.__routes__[prov].get(comar):
                        r = self.__routes__[prov].get(comar).get(ruta)
                        fbase_static = join(
                            BASE_STATIC, prov, slugify(comar), ruta)
                        fbase_photos = join(
                            BASE_PHOTOS, prov, slugify(comar), ruta)
    
                        def move_file(orig, dest):
                            whereto = join(dest, sanitize_name(orig))
                            hard_link(orig, whereto)
    
                        if(create_static):
                            for fkey in ("pdf_orig", "gpx_orig"):
                                if(fkey in r):
                                    move_file(r[fkey], fbase_static)
    
                        if(create_photos and ("pics") in r):
                            for pic in r["pics"]:
                                move_file(pic, fbase_photos)
    
        def generate_markdown(self):
            """Create markdown in the correct directory"""
            self._init_dir(BASE_PAGES)
            for prov in self.__routes__:
                for comar in self.__routes__[prov]:
                    for ruta in self.__routes__[prov].get(comar):
                        r = self.__routes__[prov].get(comar).get(ruta)
                        pages_base = join(
                            BASE_PAGES, prov, slugify(comar))
                        fpath = join(pages_base, f"{ruta}.md")
    
                        photos_base = join(prov, slugify(comar), ruta)
                        static_base = join(
                            FULL_STATIC, prov, slugify(comar), ruta)
    
                        with open(fpath, "w") as f:
                            title = "Title: "
                            if('long' in r):
                                title += r['long']
                            else:
                                title += f"{prov} - {comar} - Ruta {ruta}"
                            f.write(title + "\n")
                            f.write(f"Path: {ruta}\n")
                            f.write("Date: 2018-01-01 00:00\n")
                            if('tags' in r):
                                f.write("Tags: {}".format(", ".join(r['tags'])))
                                f.write("\n")
                            f.write("Gallery: {photo}")
                            f.write(f"{photos_base}\n")
    
                            try:
                                fpath = join("/", static_base, sanitize_name(r['pdf_orig']))
                                f.write( f'Pdf: {fpath}\n')
                            except KeyError:
                                f.write('Esta ruta no tiene descripcion (pdf)\n\n')
    
    
                            try:
                                fpath = join("/", static_base, sanitize_name(r['gpx_orig']))
                                f.write(f"Gpx: {fpath}\n")
                            except KeyError:
                                f.write('Esta ruta no tiene coordenadas (gpx)\n\n')
    
    
                            if('pics' not in r):
                                f.write('Esta ruta no tiene fotos\n\n')
    
    
    
    if __name__ == "__main__":
        routes = Routes(ORIG_BASE)
        # print(routes)
        print("done reading")
        routes.move_files()
        routes.generate_markdown()
        print("done writing")
    
  5. Destructive git behaviour

    fun with git

    I destroyed all the work I had done in a project for the last 2 months

    tl;dr:
    GIT doesnt consider the files in .gitignore important and will happily replace them

    Im pretty careless with my local git commands

    Ive been trained by git to be this careless. Unless i use --force on a command, git will always alert me if im about to do something destructive. Even then, worse case scenario, you can use git reflog to get back in time after a bad merge or something not easily accesible with a normal git flow

    What happened?

    I had a link to a folder in my master branch. I branched to do some work and decided to replace the link with the actual folder to untangle some other mess and added it to .gitignore to avoid git complaining about it

    Then happily worked on in for 2 months

    I was ready to merge it, so I made a final commit and I checked out master

    So far, pretty normal git flow… right?

    But wait, something was wrong. My folder was missing!

    Wait, what?! what happened!

    The folder existed as a syslink on master, so git happily replaced my folder with a now broken syslink

    It seems git doesnt consider files under .gitignore as important

    You can see by yourself and reproduce this behaviour by typing the following commands. It doesnt matter if links doesnt exists:

    [~/tmp]
    $ mkdir gitdestroy/
    
    [~/tmp]
    $ cd gitdestroy/
    
    [~/tmp/gitdestroy]
    $ cat > file1
    hi, im file1
    
    [~/tmp/gitdestroy]
    $ ln -s nofile link
    
    [~/tmp/gitdestroy]
    $ ll
    total 48K
    drwxr-xr-x. 26 alberto alberto  36K Jan 29 15:18 ..
    -rw-r--r--   1 alberto alberto   13 Jan 29 15:19 file1
    lrwxrwxrwx   1 alberto alberto    6 Jan 29 15:19 link -> nofile
    drwxr-xr-x   2 alberto alberto 4.0K Jan 29 15:19 .
    
    [~/tmp/gitdestroy]
    $ git init
    Initialized empty Git repository in /home/alberto/tmp/gitdestroy/.git/
    
    [~/tmp/gitdestroy (master #%)]
    $ git add -A
    
    [~/tmp/gitdestroy (master +)]
    $ git status
    On branch master
    
    No commits yet
    
    Changes to be committed:
      (use "git rm --cached <file>..." to unstage)
    
        new file:   file1
        new file:   link
    
    
    [~/tmp/gitdestroy (master +)]
    $ git commit -m "link on repo"
    [master (root-commit) 5001c61] link on repo
     2 files changed, 2 insertions(+)
     create mode 100644 file1
     create mode 120000 link
    
    [~/tmp/gitdestroy (master)]
    $ git checkout -b branchwithoutlink
    Switched to a new branch 'branchwithoutlink'
    
    [~/tmp/gitdestroy (branchwithoutlink)]
    $ git rm link 
    rm 'link'
    
    [~/tmp/gitdestroy (branchwithoutlink +)]
    $ mkdir link
    
    [~/tmp/gitdestroy (branchwithoutlink +)]
    $ cat >link/file2
    hi im file2
    
    [~/tmp/gitdestroy (branchwithoutlink +%)]
    $ cat > .gitignore
    link
    
    [~/tmp/gitdestroy (branchwithoutlink +%)]
    $ git status
    On branch branchwithoutlink
    Changes to be committed:
      (use "git reset HEAD <file>..." to unstage)
    
        deleted:    link
    
    Untracked files:
      (use "git add <file>..." to include in what will be committed)
    
        .gitignore
    
    
    [~/tmp/gitdestroy (branchwithoutlink +%)]
    $ git add -A
    
    [~/tmp/gitdestroy (branchwithoutlink +)]
    $ git commit -m "replace link with folder"
    
    [branchwithoutlink 2cfb06c] replace link with folder
     2 files changed, 1 insertion(+), 1 deletion(-)
     create mode 100644 .gitignore
     delete mode 120000 link
    
    [~/tmp/gitdestroy (branchwithoutlink)]
    $ ll
    total 60K
    drwxr-xr-x. 26 alberto alberto  36K Jan 29 15:18 ..
    -rw-r--r--   1 alberto alberto   13 Jan 29 15:19 file1
    drwxr-xr-x   2 alberto alberto 4.0K Jan 29 15:21 link
    drwxr-xr-x   4 alberto alberto 4.0K Jan 29 15:22 .
    -rw-r--r--   1 alberto alberto    5 Jan 29 15:22 .gitignore
    drwxr-xr-x   8 alberto alberto 4.0K Jan 29 15:22 .git
    
    [~/tmp/gitdestroy (branchwithoutlink)]
    $ git checkout master
    Switched to branch 'master'                                        <--- NO ERROR???
    
    [~/tmp/gitdestroy (master)]
    $ ll
    total 52K
    drwxr-xr-x. 26 alberto alberto  36K Jan 29 15:18 ..
    -rw-r--r--   1 alberto alberto   13 Jan 29 15:19 file1
    lrwxrwxrwx   1 alberto alberto    6 Jan 29 15:22 link -> nofile    <--- WHAT
    drwxr-xr-x   8 alberto alberto 4.0K Jan 29 15:22 .git
    drwxr-xr-x   3 alberto alberto 4.0K Jan 29 15:22 .
    
    [~/tmp/gitdestroy (master)]
    $ git checkout branchwithoutlink 
    Switched to branch 'branchwithoutlink'
    
    [~/tmp/gitdestroy (branchwithoutlink)]
    $ ll
    total 56K
    drwxr-xr-x. 26 alberto alberto  36K Jan 29 15:18 ..
    -rw-r--r--   1 alberto alberto   13 Jan 29 15:19 file1
    -rw-r--r--   1 alberto alberto    5 Jan 29 15:23 .gitignore
    drwxr-xr-x   8 alberto alberto 4.0K Jan 29 15:23 .git
    drwxr-xr-x   3 alberto alberto 4.0K Jan 29 15:23 .
    

    Aftermath

    I analyzed what git was doing underneath in hopes to gain some insight on how to recover these files. It seems git unlinkat(2) everyfile and finally rmdir(2) the folder

    By contrasts rm(1) just uses unlinkat(2) in every file and folder

    Not sure what difference this makes, but it was quite useless. I tried some EXT undelete tools to try to recover the missing files, but everything was gone

    Actually I was able to undeleted some files i had removed 3 years ago that i didnt need :/

    Future

    This directory was under git as well and remotely hosted. But my last push was 2 months ago. I will be more careful on the future

    Recently theres been some discussion on git about something that could prevent this behaviour. They are introducing the concept of “precious ignored” files

    But for me the damage was done

    This was unexpected behaviour for me. Maybe it was also for you. Be safe out there!

  6. New years resolution III

    Im happy to tell i successfully turned one-shot old resolutions into ingrained habits! This was a triumph. Im making a note here. HUGE SUCCESS! Its hard to overstate my satisfaction :)

    Last year resolution worked great. I feel i regained some of that juicy creativity i had as a child and that i had completely lost. This also feels like a habit now. More like a quality, which is exactly what i was aiming for

    To be completely clear about it, being creative has nothing to do with being a good creative. Absolutely everybody can be creative. Its just the first step and a requirement for everything else

    As i side-note. i was able to do a handstand without support for the first time on november. So one month ahead of schedule. Its really fun to be able to control your body while looking at the world upside down

    This year i wasnt going to explicitly make a new year resolution since i have stopped thinking of them as some change that require effort, but rather the only way forward. Not sure how it works, but the alternatives just seem bad

    But lets do it anyway for the fun of it. I was kinda doing it already without paying much attention, but im going to make it formal: reduce easy dopamine fixes. The simplest example of this is checking your phone or the news every 5 minutes or picturing conversations in your head that will never happen. But you can recognize this kind of useless satisfaction in many not-so-simple-to-spot activities if you pay attention. Ive already planned a roadmap of what this would look like

    I dont think it will take the whole year to reap some benefits out of this and ive got many more small projects planned for this year. All in all, this does look like is going to be a great year!

    To finish, a small side resolution, learning languages. Ive already been doing it for the last 6 months, but i plan on keep doing it even more this next year. Its really fun!

  7. New web Design

    I hope you like it. I remade the theme from scratch using css grid to make the web responsive. Responsive means that it adapts to the screen size, so it works both on large screens and on phones

    I tried to keep the old look as much as possible. So in the worst case scenario, you wont notice any change :)

    Web technologies have really go a long way. It used to be a nightmare. Nowadays, dare I say that is a pleasure and fun to make your own design

  8. Valencia: Bikes, Skates and beach

    A wonderful weekend in Valencia. And of course I took the chance to meet the local skaters. I also rented a bike to move throughout the city. Its a very plain city so I really enjoyed it

    I couldnt go to all the strolls they had because time was short and there were many things to do :/

    It rained a little, but I still manage to hit the beach a couple of times!

    group valencia

    stroll map

  9. Listen to accents from around the world

    localingual snapshot

    I discovered localingual with joy, just to find out is still in its infancy… but if you like languages, the promise of a great website is there!

    Right now, it suffers three major problems. Its hard to navigate, low sample size and the voting system instead of encouraging quality, encourages the oldest/jokes to be on top (unless they are very bad or too offensive, i guess). Texas have a spongebob joke right now

    Pros? If you enjoy languages, its great to hear real accents around the world

  10. Copy list of packages installed to another debian machine

    In this day and age, reading debian forums, I still see $ dpkg --get-selections as the recommended way to copy the list of packages installed on one machine in order to install the same packages on another machine

    This list misses vital information… such as which of those packages were automatically installed as dependence!!!

    If you dont want to break your new installation so early on, use $ apt-mark showmanual instead for the list of packages. It will show only packages that you manually installed. You should get the rest as dependences

  11. No more bash

    bash logo crossed

    I recently stopped my (imho bad) habit of starting shell scripts in bash; no matter how small the task at hand feels originally

    I had an epiphany

    The number of bash scripts that grew out of control was just too damn high

    ive been told that

    it's a difficult balance
    

    But is it really?

    Its always the same story

    1. Well, I only have to run the same handful of commands multiple times in different directories, a shell script will do
    2. Except, sometimes it fails when…/this special case if../oh, never considered this… I will just add a couple more lines and fix it
    3. Script explodes, and gets rewritten in python

    It rarely had exceptions for me. Almost every .sh (if its intended to automate something) had to do sanity checks, error control/recovery and probably special case scenarios… eventually

    Im aware that if you are well versed in bash, you can do a lot. It has arrays and all kind of (imho weird) string mangling to make advanced use of variables, but it always felt like bash was filled with pitfalls that you have to learn to route around

    Writting the same thing in python takes about the same time. Maybe a couple more lines to write a few imports

    Im aware that python comes with its own pitfalls, but at least you can actually scale it when needed. And you save the rewritting part

    This is really hard for me to say

    I grew to love long one liners of pipes that solve complex problems. Also, most of the time you only seem to want to run a couple of commands on tandem

    But I think its time for me to say goodbye. In the same way I said goodbye to perl (and thats a rant for another day :))

    No more shell scripts

    No matter how small

  12. Backup fixes!

    A year ago I made an automatization solution for a backup. Very basic approach but it got the job done

    It started to fail randomly, so I had to take a look. I fixed it and took the oportunity to add a few features while debugging it

    Overall improved resilience. Now it can recover from most errors and inform properly when it can not

    Changelog:

    • FIX: Backup file geting corrupted on email transit. It seems google was mangling .gpg files
    • FIX: Add clean up section to ensure the resources are consumed. Systemd.path works like a spool. Also needs to sync at the end because systemd relaunch the file as soon as is done. The OS didnt even have time to write to disk
    • FIX: Clean up service on restart that auto remove mail lock created and never removed if computer loses power in the middle of the sending
    • FIX: Systemd.path starts processing as soon as the path is found. I had to ensure the file was done written before processing it
    • FIX: Systemd forking instead of oneshot. I was leaving the process ligering for the pop up windows to finish. This is what Type=forking does

    • FEAT: Checksums included in the backup to be able to auto verify integrity when recovering and be able to properly fail when the IN and OUT files are different

    • FEAT: Add proper systemd logging. Including checksums
    • FEAT: Show POP-UPs to the final users showing star/stop of the service and notifiying them of errors
    • FEAT: Add arguments to ease local debugging including --quiet option added for debugging remotely without showing POP UPS

    No repo! but heres the code so you take a peak or reuse it. POP-UPS are in spanish

    code
    backup.py
    
    #!/usr/bin/env python3
    
    from datetime import datetime, timedelta
    from os import path, remove, fork, _exit, environ
    from subprocess import run, CalledProcessError
    from sys import exit, version_info
    from systemd import journal
    from hashlib import md5
    import argparse
    
    
    def display_alert(text, wtype="info"):
        journal.send("display: {}".format(text.replace("\n", " - ")))
        if(not args.quiet):
            if(not fork()):
                env = environ.copy()
                env.update({'DISPLAY': ':0.0', 'XAUTHORITY':
                            '/home/{}/.Xauthority'.format(USER)})
                zenity_cmd = [
                    'zenity', '--text={}'.format(text), '--no-markup', '--{}'.format(wtype), '--no-wrap']
                run(zenity_cmd, env=env)
                # let the main thread do the clean up
                _exit(0)
    
    
    def md5sum(fname):
        cs = md5()
        with open(fname, "rb") as f:
            for chunk in iter(lambda: f.read(4096), b""):
                cs.update(chunk)
        return cs.hexdigest()
    
    
    # Args Parser init
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "-q", "--quiet", help="dont show pop ups", action="store_true")
    parser.add_argument("-u", "--user", help="user to display the dialogs as")
    parser.add_argument("-p", "--path", help="path of the file to backup")
    parser.add_argument("-t", "--to", help="who to send the email")
    parser.add_argument(
        "-k", "--keep", help="keep output file", action="store_true")
    parser.add_argument(
        "-n", "--no-mail", help="dont try to send the mail", action="store_true")
    args = parser.parse_args()
    
    # Globals
    USER = 'company'
    if(args.user):
        USER = args.user
        journal.send("USER OVERWRITE: {}".format(USER))
    
    TO = "info@company.com"
    if(args.to):
        TO = args.to
        journal.send("EMAIL TO OVERWRITE: {}".format(TO))
    BODY = "mail.body"
    FILENAME = 'database.mdb'
    PATH = '/home/company/shared'
    if(args.path):
        PATH = args.path
        journal.send("PATH OVERWRITE: {}".format(PATH))
    
    if(args.quiet):
        journal.send("QUIET NO-POPUPS mode")
    
    FILE = path.join(PATH, FILENAME)
    FILEXZ = FILE + ".tar.xz"
    now = datetime.now()
    OUTPUT = path.join(PATH, 'backup_{:%Y%m%d_%H%M%S}.backup'.format(now))
    CHECKSUM_FILE = FILENAME + ".checksum"
    
    error_msg_tail = "Ejecuta $ journalctl -u backup.service para saber más"
    
    LSOF_CMD = ["fuser", FILE]
    XZ_CMD = ["tar", "-cJC", PATH, "-f", FILEXZ, FILENAME, CHECKSUM_FILE]
    GPG_CMD = ["gpg", "-q", "--batch", "--yes", "-e", "-r", "backup", "-o", OUTPUT, FILEXZ]
    
    error = ""
    
    
    # Main
    display_alert('Empezando la copia de seguridad: {:%Y-%m-%d %H:%M:%S}\n\n'
                  'NO apagues el ordenador todavia por favor'.format(now))
    
    # sanity file exists
    if(path.exists(FILE)):
        journal.send(
            "New file {} detected. Trying to generate {}".format(FILE, OUTPUT))
    else:
        exit("{} not found. Aborting".format(FILE))
    
    # make sure file finished being copied
    finished_copy = False
    while(not finished_copy):
        try:
            run(LSOF_CMD, check=True)
            journal.send(
                "File is still open somewhere. Waiting 1 extra second before processing")
            run("sleep 1".split())
        except CalledProcessError:
            finished_copy = True
        except Exception as e:
            display_alert(
                "ERROR\n{}\n\n{}".format(e, error_msg_tail), "error")
            exit(0)
    
    filedate = datetime.fromtimestamp(path.getmtime(FILE))
    
    # sanity date
    if(now - timedelta(hours=1) > filedate):
        error = """El fichero que estas mandando se creó hace más de una hora.
    fecha del fichero: {:%Y-%m-%d %H:%M:%S}
    fecha actual     : {:%Y-%m-%d %H:%M:%S}
    
    Comprueba que es el correcto
    """.format(filedate, now)
    
    # Generate checksum file
    csum = md5sum(FILE)
    journal.send(".mdb md5: {} {}".format(csum, FILENAME))
    
    with open(CHECKSUM_FILE, "w") as f:
        f.write(csum)
        f.write(" ")
        f.write(FILENAME)
    
    # Compress
    if(path.isfile(FILEXZ)):
        remove(FILEXZ)
    
    journal.send("running XZ_CMD: {}".format(" ".join(XZ_CMD)))
    run(XZ_CMD)
    csum = md5sum(FILEXZ)
    journal.send(".tar.xz md5: {} {}".format(csum, FILEXZ))
    
    # encrypt
    journal.send("running GPG_CMD: {}".format(" ".join(GPG_CMD)))
    run(GPG_CMD)
    csum = md5sum(OUTPUT)
    journal.send(".gpg md5: {} {}".format(csum, OUTPUT))
    
    remove(FILEXZ)
    
    # sanity size
    filesize = path.getsize(OUTPUT)
    if(filesize < 5000000):
        error += """"El fichero que estas mandando es menor de 5Mb
    tamaño del fichero en bytes: ({})
    
    Comprueba que es el correcto
    """.format(filesize)
    
    subjectstr = "Backup {}ok con fecha {:%Y-%m-%d %H:%M:%S}"
    subject = subjectstr.format("NO " if error else "", now)
    body = """Todo parece okay, pero no olvides comprobar que
    el fichero salvado funciona bien por tu cuenta!
    """
    if(error):
        body = error
    
    with open(BODY, "w") as f:
        f.write(body)
    
    journal.send("{} generated correctly".format(OUTPUT))
    try:
        if(not args.no_mail):
            journal.send("Trying to send it to {}".format(TO))
            MAIL_CMD = ["mutt", "-a", OUTPUT, "-s", subject, "--", TO]
    
            if(version_info.minor < 6):
                run(MAIL_CMD, input=body, universal_newlines=True, check=True)
            else:
                run(MAIL_CMD, input=body, encoding="utf-8", check=True)
    except Exception as e:
        display_alert(
            "ERROR al enviar el backup por correo:\n{}".format(e), "error")
    else:
        later = datetime.now()
        took = later.replace(microsecond=0) - now.replace(microsecond=0)
        display_alert('Copia finalizada: {:%Y-%m-%d %H:%M:%S}\n'
                      'Ha tardado: {}\n\n'
                      'Ya puedes apagar el ordenador'.format(later, took))
    
    finally:
        if(not args.keep and path.exists(OUTPUT)):
            journal.send("removing gpg:{}".format(OUTPUT))
            remove(OUTPUT)
    
    unbackup.py
    #!/usr/bin/env python3
    
    from os import path, remove, sync, fork, _exit, environ
    from subprocess import run, CalledProcessError
    from glob import glob
    from sys import exit
    from systemd import journal
    from hashlib import md5
    import argparse
    
    
    def display_alert(text, wtype="info"):
        if(not args.quiet):
            if(not fork()):
                env = environ.copy()
                env.update({'DISPLAY': ':0.0', 'XAUTHORITY':
                            '/home/{}/.Xauthority'.format(USER)})
                zenity_cmd = [
                    'zenity', '--text={}'.format(text), '--no-markup', '--{}'.format(wtype), '--no-wrap']
                run(zenity_cmd, env=env)
                # Let the main thread do the clean up
                _exit(0)
    
    
    def md5sum(fname):
        cs = md5()
        with open(fname, "rb") as f:
            for chunk in iter(lambda: f.read(4096), b""):
                cs.update(chunk)
        return cs.hexdigest()
    
    
    # Args Parser init
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "-q", "--quiet", help="dont show pop ups", action="store_true")
    parser.add_argument("-u", "--user", help="user to display the dialogs as")
    parser.add_argument("-p", "--path", help="path of the file to unbackup")
    parser.add_argument(
        "-k", "--keep", help="keep original file", action="store_true")
    args = parser.parse_args()
    
    # Globals
    USER = 'company'
    if(args.user):
        USER = args.user
        journal.send("USER OVERWRITE: {}".format(USER))
    
    PATH = '/home/rk/shared'
    if(args.path):
        PATH = args.path
        journal.send("PATH OVERWRITE: {}".format(PATH))
    
    if(args.quiet):
        journal.send("QUIET NO-POPUPS mode")
    
    OUTPUT_FILE = 'database.mdb'
    error_msg_tail = "Ejecuta $ journalctl -u unbackup.service para saber más"
    CHECKSUM_FILE = OUTPUT_FILE + ".checksum"
    
    
    # Main
    try:
        input_file = glob(path.join(PATH, 'backup*.backup'))[0]
    except IndexError as e:
        display_alert("ERROR\nEl fichero de backup no existe:\n{}\n\n{}".format(
            e, error_msg_tail), "error")
        exit(0)
    except Exception as e:
        display_alert(
            "ERROR\n{}\n{}".format(e, error_msg_tail), "error")
        exit(0)
    else:
        display_alert(
            "Se ha detectado {}. Empiezo a procesarlo".format(input_file))
    
        output_path = path.join(PATH, OUTPUT_FILE)
        output_pathxz = output_path + ".tar.xz"
    
        LSOF_CMD = ["fuser", input_file]
        GPG_CMD = ["gpg", "--batch", "-qdo", output_pathxz, input_file]
        XZ_CMD = ["tar", "-xf", output_pathxz]
    
    # make sure file finished being copied. Systemd triggers this script as soon as the file name shows
    try:
        finished_copy = False
        while(not finished_copy):
            try:
                run(LSOF_CMD, check=True)
                journal.send(
                    "File is still open somewhere. Waiting 1 extra second before processing")
                run("sleep 1".split())
            except CalledProcessError:
                finished_copy = True
            except Exception as e:
                display_alert(
                    "ERROR\n{}\n\n{}".format(e, error_msg_tail), "error")
                exit(0)
    
        csum = md5sum(input_file)
        journal.send(".gpg md5: {} {}".format(csum, input_file))
    
        if(path.exists(output_pathxz)):
            journal.send("{} detected. Removing".format(output_pathxz))
            remove(output_pathxz)
    
        journal.send("running GPG_CMD: {}".format(" ".join(GPG_CMD)))
        run(GPG_CMD, check=True)
    
        csum = md5sum(output_pathxz)
        journal.send("tar.xz md5: {} {}".format(csum, input_file))
    
        journal.send("running XZ_CMD: {}".format(" ".join(XZ_CMD)))
        run(XZ_CMD, check=True)
    
    # Check Checksum
        with open(CHECKSUM_FILE) as f:
            target_cs, filename = f.read().strip().split()
        actual_cs = md5sum(filename)
        journal.send(".mdb md5: {} {}".format(actual_cs, filename))
        if(target_cs == actual_cs):
            journal.send("El checksum interno final es correcto!")
        else:
            display_alert("ERROR\n"
                          "Los checksums de {} no coinciden"
                          "Que significa que el fichero esta dañado"
                          .format(filename), "error")
    
    except Exception as e:
        display_alert("ERROR\n{}\n\n{}"
                      .format(e, error_msg_tail), "error")
        exit(0)
    else:
        display_alert("{} generado con exito".format(output_path))
    finally:
        if(not args.keep and path.exists(input_file)):
            journal.send("CLEAN UP: removing gpg {}".format(input_file))
            # make sure the file is not open before trying to remove it
            sync()
            remove(input_file)
            # sync so systemd dont detect the file again after finishing the script
            sync()
    
    backup.path 
    [Unit]
    Description=Carpeta Compartida backup
    
    [Path]
    PathChanged=/home/company/shared/database.mdb
    Unit=backup.service
    
    [Install]
    WantedBy=multi-user.target
    
    backup.service
    [Unit]
    Description=backup service
    
    [Service]
    Type=forking
    ExecStart=/root/backup/backup.py
    TimeoutSec=600
    
    unbackup.path
    [Unit]
    Description=Unbackup shared folder
    
    [Path]
    PathExistsGlob=/home/company/shared/backup*.backup
    Unit=unbackup.service
    
    [Install]
    WantedBy=multi-user.target
    
    unbackup.service
    [Unit]
    Description=Unbackup service
    [Service]
    Type=forking
    Environment=DISPLAY=:0.0
    ExecStart=/root/company/unbackup.py
    
  13. Gmail mangles .gpg files

    Why?

    I dont know

    If you change bytes in a .gpg somebody is bound to notice. Right?

    Im using a 3rd party to send a .gpg to a gmail account and the checksums before and after simply dont match

    I dont want to really assume evilness, since modifiying bytes on the attachments seems pretty sketchy

    Maybe im doing something wrong

    The fact that the checksums are okay if I send it to my personal server using the same 3rd party mail provider its a little suspicious tho

    Ive been told that

    [...], email and gmail are different things.  So I wouldn't be surprised if they are not 100% compatible ;-)
    

    Funny, or is it? google has most of my email because it has all of yours. They have a lot of leverage to define the email experience

    In the end, just renaming the .gpg to something else fixed it (What??)

    And while we are still ranting about google, lets finish with a pet peeve of mine

    I hate that they virtually remove the concept of domain or email address. Not that they are just the f*cking anchor point to security

    Security and, you know, knowing where the f*ck you are going or who you are trying to get in contact with

    Instead they hide this info as much as possible in labels so you learn to trust them, instead of learning to trust something as simple and as ubiquitous as a domain or an email

  14. You have no repos online!

    Git is life. I manage my own git server in git.alberto.tf. But its now set private to only committers

    The main reason is that, more often than not, im the only committer. That allows me to be carefree about using time based commits (as opposed to features-based commits as god intended) when I need it. For example to move my work from one computer to another, etc…

    Im also less careful about clumping together typos, features and fixes on the same commits

    Just by having many time-based commits make the repos themselves to not add much to the code I show otherwise. So I decided to not expose the repos by default. You are still free to use any code you find on this site

    The other reason is metadata

    Avoid awkward faces when I have to explain a commit at 4am. – “I thought you were sick!” :)

  15. New years resolution II

    I like the idea of deliberately thinking and working on some kind self improvement task that affects you in a global sense

    One-shot changes that have a high ROI. Like changing a habit

    Last year resolution worked great and I think its been a success :)

    This years resolution will be to do more creative work. It might not look like it, but the more I do it, the more I think it is just a habit. It didnt come naturally to me, but after awhile, you actually crave for it when you cant do it

    Ive been using this method called the single word method

    Basically, you just have to add something, no matter how small, to whatever you are doing. After that, you are allowed to to quit for the day

    More often than not, what happens is that ‘one word’ end up being quite a lot because you inadvertently enter “the zone”

    And while we are still talking about resolutions one last adition. It wasnt really a resolution because it just happened a year after I quited smoking, but after 2 years I can say Im really into doing sports now

    Im really past the point when I gotta worry about underdoing and have to worry about overdoing to avoid injuries. Ive shunned doing exercise for a very long time. This has not been easy. It has not been hard either, but I could totally hear my body asking me what the heck was i doing moving so much and asking me to stop

    Working out now is part of my life in a way that I will miss when I will no longer able to do it

    PS: Side goal for this year: to do a handstand without wall support

  16. Virus, Qubes-OS and Debian

    computer problems that people attribute to virus doesnt overlap with real problems caused by virus

    This is the virus venn diagram. Its pretty accurate and many people, including people that gets along with technology, is oblivious to it. Voluntarily installing crap by installing random programs you just googled in your computer hardly counts as a virus

    Sometimes they overlap tho. What I call “trawling viruses”. Using some very old exploit that should hardly work on anybody and spamming it, you can still get lots of people that never update. In this case, you dont care about anything, you just try get a quick profit and you dont really care if you slow down the target machine

    But by and large, virus try to be as invisible as possible, do their bussiness and go undetected for as long as possible. If they can make an optimization to your system, like patching how they got in, they will

    Using debian is one way to protect yourself… but they still fall short because it still uses a very old authorization model

    Authorization model in computers is old

    Its no secret that the authorization model in computers is really old

    Qubes-os is a system that tries to mitigates that problem quite sucessfully. Qubes-os 4.0 rc1 has been released recently. Im currently testing it on my mediabox, and will probably use it in my main machine soon

    Holger gave a talk a few weeks ago named “Using qubes os from the pov of a debian developer”. In debconf fashion you can watch it online

  17. Shakira digs it

    […]

    She makes a man wants to speak Spanish

    Como se llama (si), bonita (si), mi casa (si, Shakira Shakira), su casa

    Shakira, Shakira”

    Oh baby when you talk like that

    You make a woman go mad”

    – Shakira, my hips done lie

    Roughly translates to “hows the name/hows your name” (politelty if we assume hes addressing to her), pretty(yes), my house, your house, my pen, your book. The last two, not actually mentioned in the song, but they just seem the logical follower to those first steps in spanish

    If this broken spanish is enough to make not only her, but any woman go mad as she suggest, you can pretty much tell shes digging something else

  18. Stretch is out

    New version of Debian is out… codename stretch. Thats the name of the stretchy octopus toy from Toy Story 3

    All the code names of debian come from Toy story

    For normal desktops users of the testing branch, updates come incremmentally and we have been using stretch without problems for a while now, but stable deployments have a new version to jump to when ready!

    In my experience, for testing users, its better to stay in stable for a while (at least 2-3 months) before jumping into testing again. Theres a lot of new packages waiting to enter debian and it takes a while to stabilize again. Developers has been asked to spread out their uploads a little so not everything breaks at once

    Also, the first point release is expected in a month. So wait out that long for a smoother transtion of your stable installations

  19. Spanish people making up english words

    Its funny how a collective of people can make up and agree upon the meaning of a foreign word or even fabricate a whole new word out of thin air

    I was talking with a friend about how she was getting braces and she called them brackets instead. I realized that brackets was the spanish word for braces!

    These are brackets in english –> [ ]

    Then I realized theres plenty of examples: zapping (channel surfing), being a crack (being someone exceptionally good at something), footing (jogging),… the list goes on and on

    Im sure they all have a distint origin and story behind them than over a period of time became ingrained in all us as a collective without noticing it

    Can you think of something similar happening in english or your mother tongue? Please, let me know, Im always curious about this kind of quirks of the language :)

  20. Get updates from this blog!

    Have you ever seen this icon and wonder what it is?

    You can see this refered as RSS, atom or just feed of a website. It all refers to the same thing: get updates from this site without having to create an account

    Its likely you never heard of this tech because it has been supressed and practically been eliminated from all mainstream webs despite being simple and functional. Google had one of the best RSS readers and they closed it

    The reason for this is that they want to be your only stop for information. You might not think much about it but your attention is the most valuable asset right now… for you and for them. They can try to push information (propaganda or ads) on you like you are some kind of kid who doesnt know better

    Luckily, this tech is so simple that is likely never going to die as long as a single reader and a single feed poster exists on the planet :)

    This is an example of what a feed look like and this is list with all my feeds

    Theres a plethora of offline readers out there both for computer and cellphone and you only have to copy the feed link into it. Ask me if you have troubles making it work or finding one that you like

    I host my own copy of tt-rss, an online reader, and it allows multiple users. If you know me, just ask me for an user and I will happily create one for you so you dont need to host your own (an online reader allows to sync all of your devices for example)

  21. Gymkhana

    In Spain a Gymkhana refers to a competion game where you have a set of random trials that people have to complete in order to reach the next

    They are usually already fun on their own, but how could we improve upon it? You guessed it right, by doing it on wheels

    Our first trial was to reach point A and have each one of us hold one wheel of one of team members

    trial #1

    Second trial was to reach point B and find a bike to hold

    trial #2

    And the third trial was to reach point C and have a couple of strangers dance with us

    All in all, we had a blast. We finished in third place too. Not too shabby

    group photo

  22. Music online

    $ du -sh ~/Music
    168G
    

    This is my whole music collection.

    At least half of it either I never listened to it or Im not very fond of. It includes whole discographies and whole mp3 collections from friends

    Skating is much more enjoyable with some music. I have a speaker I carry around with a 2Gb microsd card from an old phone. I usually just drop a few random albums or compilations I like

    I tried a shared list on spotify, but half of the songs I wanted to add werent available or they didnt have the specific version I like

    Recently I had to buy some memories, and saw the drop in prices! A 128Gb usb(2.0) is around 20€ and a 128Gb MicroSD for around 33€. Now I can carry my whole collection with me around. No lag, no data expenses, the version I like…

    Who wants music to be online?

  23. Skates and scuba diving in Zaragoza

    This was a very complete and long weekend that started very early on thursday

    Thursday: Dora!

    Los verdes Thusdays “Dora stroll”

    dora

    Friday: Madrid Friday Night Skate!

    Once a month

    mfns 1

    mfns 2

    Saturday: Skating in Zaragoza and scuba diving!

    Early in the morning we headed to Zaragoza. We had the first stroll at 11am with Patinar Zaragoza

    Zaragoza

    Zaragoza

    And Scuba diving on the biggest river aquarium in Europe

    scuba

    scuba

    scuba

    scuba

    scuba

    scuba

    scuba

    Sunday: Zaragoza Roller!

    Finally we skated some Zaragoza roller club called Zaragoza Roller

    zaragoza

    zaragoza

    zaragoza

    zaragoza

    zaragoza

    zaragoza

    In the zaragoza expo preparing for leaving:

  24. Automating the extraction of duplicated zip files

    Its not that well-known that a zip file does not save a directory inside. It saves a secuence of files, and nothing prevents those files names to be duplicated inside a file

    All the tools Ive checked out overwrite silently the duplicates or allow you to manually rename them. Which is very tedious as soon as you have to do this a few times with lots of duplicated files

    I had to bake my own solution using python. If you know about a tool that does this, please let me know. I love to deprecate my own solutions :)

    unzip_rename_dups.py
    
    #!/usr/bin/env python3
    import pdb
    import sys
    import zipfile
    from os.path import splitext, dirname, abspath, join
    from os import rename
    
    
    ZIP = sys.argv[1]
    DIR = dirname(abspath(ZIP))
    
    filenames = {}
    extracted = 0
    dups = 0
    
    with zipfile.ZipFile(ZIP) as z:
    
        for info in z.infolist():
            z.extract(info, DIR)
            extracted += 1
    
            fn = info.filename
    
            if fn not in filenames:
                filenames[fn] = 1
            else:
                filenames[fn] += 1
                dups += 1
    
            orig_path = join(DIR, fn)
    
            preext, postext = splitext(fn)
            final_fn = preext + str(filenames[fn]) + postext
            final_path = join(DIR, final_fn)
    
            rename(orig_path, final_path)
    
    print("{} files extracted sucesfully. {} Duplicated files saved!".format(extracted, dups))
    
  25. Automatize wildcard cert renewal

    problem definition

    I host one instance of sandstorm. Id like to use my own domain AND HTTPS

    Sandstorm uses a new unguessable throw-away host name for every session as part of its security strategy, so in order to host your own under your own domain, you need a wildcard DNS entry and a wildcard cert for it (a cert with a *.yourdomain that will be valid for all your subdomains)

    I use certbot (aka letsencrypt) to generate my certificates. Unfortunately, they have stated that will not emit wildcard certificates. Not now, and very likely, not in the future

    Sandstorm offers a free DNS service using sandcats.io with batteries included (free wildcard cert). But this makes the whole site looks like they are not running under your control when you share a link to it to a third party (even tho is not true). This being one of the main points of running my own instance makes this solution not suitable for me

    For reasons that deserver its own rant, I will not buy a wildcard cert

    This only left me with the option of running sandstorm in a local port, have my apache proxy petitions and present the right certs. I will be using the sandcats.io DNS + wilcard cert for websockets, which are virtually invisible to the final user

    The certbot cert renovation is easy enough to automate, but I need to automate the renewal of the sandcats.io cert, which lasts for 9 days

    solution

    A service will run weekly to renew the cert. For this, It will use a configuration faking using one of those free sandcats.io free certs so sandstorm renew the cert. Parse the new cert and tell apache to use it

    shortcomings

    Disclaimer: This setup is not officially supported by sandstorm

    The reason is that some apps doesnt work well due to some browsers security policies. Just like sandstorm guys, I had to make a compromise. The stuff I use works for me and I have to test it before I use something new :)

    code
    updatecert.py
    
    #!/usr/bin/env python3
    import json
    from subprocess import call,check_call
    from glob import glob
    from shutil import copy
    from time import sleep
    from timeout import timeout
    
    TIMEOUT = 120
    
    SSPATH = '/opt/sandstorm'
    CONF = SSPATH + '/sandstorm.conf'
    GOODCONF = SSPATH + '/sandstorm.good.conf'
    CERTCONF = SSPATH + '/sandstorm.certs.conf'
    CERTSPATH = SSPATH + '/var/sandcats/https/server.sandcats.io/'
    APACHECERT = '/etc/apache2/tls/cert'
    APACHECERTPUB = APACHECERT + '.crt'
    APACHECERTKEY = APACHECERT + '.key'
    
    RESTART_APACHE_CMD = 'systemctl restart apache2'.split()
    RESTART_SS_CMD = 'systemctl restart sandstorm'.split()
    
    @timeout(TIMEOUT, "ERROR: Cert didnt renew in {} secs".format(TIMEOUT))
    def check_cert_reply(files_before):
        found = None
        print("waiting for new cert in" + CERTCONF, end="")
        while not found:
            print(".", end="", flush=True)
            sleep(5)
            files_after = set(glob(CERTSPATH + '*.response-json'))
    
            found = files_after - files_before
        else:
            print("")
        return found.pop()
    
    def renew_cert():
        files_before = set(glob(CERTSPATH + '*.response-json'))
        copy(CERTCONF, CONF)
        call(RESTART_SS_CMD)
        try:
            new_cert = check_cert_reply(files_before)
        finally:
            print("Restoring sandstorm conf and restarting it")
            copy(GOODCONF, CONF)
            call(RESTART_SS_CMD)
            print("Restoring done")
        return new_cert
    
    def parse_cert(certfile):
        with open(certfile) as f:
            certs = json.load(f)
    
        with open(APACHECERTPUB, 'w') as cert:
    
            cert.write(certs['cert'])
    
            ca = certs['ca']
            ca.reverse()
            for i in ca:
                cert.write('\n')
                cert.write(i)
    
        copy(certfile[:-len('.response-json')], APACHECERTKEY)
    
    if __name__ == '__main__':
        new_cert = renew_cert()
        parse_cert(new_cert)
        try:
            check_call(RESTART_APACHE_CMD)
        except:
            # one reason for apache to fail is to try to parse the json before is completely written
            # try once again just in case
            print("failed to restart apache with the new cert. Trying once more")
            sleep(1)
            parse_cert(new_cert)
            call(RESTART_APACHE_CMD)
    
    updatecert.service
    
    [Unit]
    Description=tries to renew ss cert
    OnFailure=status-email-admin@%n.service
    
    [Service]
    Type=oneshot
    ExecStart=/root/updatecert.py
    
    updatecert.timer
    
    [Unit]
    Description=runs ss cert renewal once a week
    
    [Timer]
    Persistent=true
    OnCalendar=weekly
    Unit=updatecert.service
    
    [Install]
    WantedBy=default.target
    
  26. Life-time perspective

    This is my screen wallpaper. It uses a generous average of 90 years for a life-time and theres one square per week

    Every monday I colour one week

    Ive been doing this for about 6 months now and it does help keep things in perspective

    template weeks in a life-time calendar

    PS: To do the initial coloring I used gimp + xdotool

    paint.sh
    
    #!/bin/bash
    
    debug=1
    #years should be your age - 1
    years=10
    
    xdotool click $debug
    for i in $(seq 0 10); do
        for j in $(seq 0 $years); do
            xdotool click $debug
            xdotool mousemove_relative -- 18 0
    
        done
        xdotool click $debug
        xdotool mousemove_relative -- 0 -20
    
        for j in $(seq 0 $years); do
            xdotool click $debug
            xdotool mousemove_relative -- -18 0
    
        done
        xdotool click $debug
        xdotool mousemove_relative  -- 0 -20
    done
    
  27. Taking things at face value

    Yesterday I wrote about my new years resolution and used this expression in a negative sense

    That same evening I saw the same expresison in an article used in a positive light. As in things should be taken at face value. Without looking for hidden intentions

    I researched it and this expression was “coined” refering to actual coins and meaning “trust the face in the coin”, which represents its value

    While not completely wrong, I realized this expression had to do more with trust that I wanted to imply with look beyond. So I replaced it with theres more than meets the eye, which is more generic and conveys better what I meant to say

  28. New years resolution

    Mine is a little different from the conventional ones this year

    Look beyond
    

    A fancy version of dont take things at face value theres more than meets the eye. Look for extra connections

    Only a few days past new years eve and I think its already paying off for me :)

  29. Small automatic Backup using python

    EDIT: Newer version available

    problem definition

    An automatic backup of a database file inside a legacy windows virtual machine without internet access.

    The client doesnt have a dedicated online machine for backups and the backup should “leave the building”

    solution

    A shared folder using virtuabox shared folders facilities. The database will be copied once a day

    Outside the VM systemd will monitor the copy and launch the backup script

    The backup will be compress using XZ and encrypted using gpg with an asymetric key

    Finally, it will be sent for storage to one mail account where they can check if the backup was made

    code
    backup.py
    
    #!/usr/bin/env python3
    
    from datetime import datetime, timedelta
    from os import path, remove
    from subprocess import run
    from sys import exit
    
    now = datetime.now()
    
    TO = "info@company.com"
    BODY = "mail.body"
    FILENAME = 'database.mdb'
    PATH = '/home/company/shared'
    FILE = path.join(PATH, FILENAME)
    FILEXZ = FILE + ".xz"
    OUTPUT = path.join(PATH, 'backup_{:%Y%m%d_%H%M%S}.gpg'.format(now))
    
    XZ_CMD = "xz -k {}"
    GPG_CMD = "gpg -q --batch --yes -e -r rk -o {} {}"
    MAIL_CMD = "mutt -a {} -s '{}' -- {} < {}"
    
    error = ""
    
    # sanity file exists
    if path.exists(FILE):
        print("New file {} detected. Trying to generate {}".format(FILE, OUTPUT))
    else:
        exit("{} not found. Aborting".format(FILE))
    
    
    filedate = datetime.fromtimestamp(path.getmtime(FILE))
    
    # sanity date
    if now - timedelta(hours=1) > filedate:
        error = """The file you are sending was created 1+ hour ago
    file date   : {:%Y-%m-%d %H:%M:%S}
    current date: {:%Y-%m-%d %H:%M:%S}
    
    Please check if its the correct one
    """.format(filedate, now)
    
    # Compress
    if path.isfile(FILEXZ):
        remove(FILEXZ)
    
    run(XZ_CMD.format(FILE).split())
    
    # encrypt
    run(GPG_CMD.format(OUTPUT, FILEXZ).split())
    remove(FILEXZ)
    
    # sanity size
    filesize = path.getsize(OUTPUT)
    if filesize < 5000000:
        error += """"The size of the file you are sending is < 5Mb
    File size in bytes: ({})
    
    Please, Check if its the correct one
    """.format(filesize)
    
    subjectstr = "Backup {}ok with date {:%Y-%m-%d %H:%M:%S}"
    subject = subjectstr.format("NOT " if error else "", now)
    body = """Everything seems okay, but dont forget to check
    manually if the saved file works okay once in a while!
    """
    if error:
        body = error
    
    with open(BODY, "w") as f:
        f.write(body)
    
    print("{} generated correctly. Trying to send it to {}".format(OUTPUT, TO))
    run(MAIL_CMD.format(OUTPUT, subject, TO, BODY), shell=True)
    remove(OUTPUT)
    

    Inside the VM using the scheduler

    backup.bat
    
    @echo off
    xcopy /Y C:\program\database.mdb z:\
    

    mutt conf file

    .muttrc
    
    set sendmail="/usr/bin/msmtp"
    set use_from=yes
    set realname="Backup"
    set from=backup@company.com
    set envelope_from=yes
    

    systemd files

    shared.service
    
    [Unit]
    Description=company backup service
    
    [Service]
    Type=oneshot
    ExecStart=/root/backup/backup.py
    
    shared.path
    
    [Unit]
    Description=shared file
    
    [Path]
    PathChanged=/home/company/shared/database.mdb
    Unit=shared.service
    
    [Install]
    WantedBy=multi-user.target
    
  30. Dealing with mime types

    The mime type is a little text like image/jpeg or application/pdf that is used to identify the content of a file. Its main use is to determine what program can handle each file

    Theres been this problem where the default program offered by debian were a very awkward one. Like trying to use the notepad.exe that comes with wine to open .txt files. In linux we dont rely on extensions to determine the content of a file, but still wines registers itself as being able to handle text/plain files using its wine-extension-txt.desktop file

    The reason this happens is that in some lightweight desktops (like xfce), it will default to the last program installed (or upgraded!!!) that can handle the file, unless you define a default one yourself

    I fixed this weird behaviour a long time ago by using strace |grep home to locate the file that was being used and updating it. In this case

    ~/.local/share/applications/mimeapps.list
    

    It was fast, but I never really understood why it happened on the first place

    This post found on planet debian digs a little deeper using a different but longer approach and was used to find the reason behind it. Good catch!

  31. Madrid Friday Night Skate

    A few people are trying to make a regular skating event once a month to join other skating cities such as Zurich, London or New York

    A couple of weeks ago we had our 4th session! I dare you to join us next time!!

    Group photo Madrid Friday Night Skate 2016 11 18

  32. Words beauty: Sentinel

    noun
    a soldier or guard whose job is to stand and keep watch.
    verb
    station a soldier or guard by (a place) to keep watch.
  33. Green great dragons cant exist

    Natives (and some non-natives after a while!) can tell if something sounds off, even if they cant explain why. One of those things is adjetive order. They follow some rules that even people using english for all their lives are able to explain because it just sounds natural to them

    This order is

    quantity/number - quality/opinion - size - age - shape - color - place of origin - material - purpose NOUN
    

    This is why there cant be any green great dragon, but only great green dragons

    There seems to be some exceptions to this, like big bad wolf, but in my opinion, bad is not an opinion of what you think of the wolf, but part of the name, as in big bad-wolf :)

    proper adjetives order

  34. Sobering in 2016

    Camacho

    Trump winning 2016 elections in US has been sobering. I was certainly in denial about something there

    Brexit tells a similar story as well

    Rajoy winning on my own country was not enough since I consider they cheated. They managed to get rid of the second candidate that the people legitimately voted for and managed to swing the final results.

    The raw number of voters that Rajoy had should had been enough to sober me up, but the truth is that they werent that many. The winner was abstention by a landslide and the total number of people that voted something else was way higher

    Trump and brexit won with over 50% of voters.

    This is something different

  35. Bringing existing repo into gitolite

    The proposition on the original docs is slightly complex because it involves bringing the bare repo to the server, check a lot of things and run exotic gitolite commands

    My solution is easier and without having to touch anything server side

    1. Create the repo in gitolite-admin/conf/gitolite.conf and push it. This creates an empty bare repo remotely

    2. Go to some copy of the existing repo you want to move to gitolite

      $ #Configure your repo url as origin
      $ git remote add origin your.gitserver:repo.name.from.step1
      $ #Push the repo
      $ git push origin master
      $ #Assign current branch (master) to origin/master
      $ git branch --set-upstream-to=origin/master
      
    3. Profit!!!

  36. For a rainy day

    When it rains you gotta look for something to do indoors. How about some rolling dance?

    photo rollerdance

  37. quote on privacy

    Google is an over-eager octopus, but Facebook crosses the line into hentai”

    — Some random slashdot commenter

  38. How to create your own markdown syntax with python-markdown

    Rationale

    Markdown (and I use Pelican with md) is not very good at handling images. I want an easy way in md for the image to fill the column but to link to the full size image automatically

    I want to turn:

    !![alt text](path/image.png title text)
    

    into:

    <a href="path/image.png"><img width=100% title="title text" alt="alt text" src="path/image.png"></a>
    
    Code
    aimg/__init__.py:
    
    #!/usr/bin/python
    # mardown extension. Wraps <a> tags around img and adds width=100%
    #
    # This makes easier to link to big images by making them fit the column
    # and linking to the big image
    #
    # run the module to check that it works :)
    
    from markdown.extensions import Extension
    from markdown.inlinepatterns import Pattern
    from markdown.util import etree
    
    class Aimg(Extension):
        def extendMarkdown(self, md, md_globals):
            md.inlinePatterns.add('aimg', AimgPattern('^!!\[(.+)\]\((\S+) (.+)\)$'), '_begin')
    
    
    class AimgPattern(Pattern):
        def handleMatch(self, m):
            a = etree.Element('a', {'href':m.group(3)})
            img = etree.Element('img', {
                'width': '100%',
                'src': m.group(3),
                'alt': m.group(2),
                'title': m.group(4)
            })
            a.append(img)
            return a
    
    if __name__ == '__main__':
        import markdown
        print(markdown.markdown('!![alt text](/images/image.png title text)', extensions=[Aimg()]))
    

    In your pelicanconf.py:

    import aimg
    MD_EXTENSIONS = ['codehilite(css_class=highlight)', 'extra', aimg.Aimg()]
    
    Bonus
    alternative solutions without an extension

    Yes, you can insert raw HTML in a markdown file

    <a href="path/image.png"><img width=100% title="title text" alt="alt text" src="path/image.png"></a>
    

    Yes, you can have them mixed. You cant add attributes tho

    [<img width=100% title="title text" alt="alt text" src="path/image.png">](path/image.png)
    

    Yes, with the extra extension you can have classes and modify them via CSS

    ![alt text](path/image.png title text){.classnamewith100%width}
    
    My version
    !![alt text](path/image.png title text)
    
  39. Look at that nice looking FreedomBox!

    I’m rebuilding my home server and decided to take a look at freedombox project as the base for it

    0.6 version was recently released and I wasnt aware of how advanced the project is already!

    They have a virtualbox image ready for some quick test. It took me longer to download it than to start using it

    Here’s a pic of what it looks like to entice you to try it :)

    freedombox snapshot

    All this is already on debian right now and you can turn any debian sid installation into a freedombox just by installing a package

    The setup generates everything private on the first run, so even the virtualbox image can be used as the final thing

    They use plinth (django) to integrate the applications into the web interface. More info on how to help integrate more debian packages here

    A live demo is going to be streamed this friday and a hackaton is scheduled for this saturday

    Cheers!

    Original post at Laura Arjona’s Blog on 30 October 2015. Thanks for first hosting it!

¡ En Español !